Jake Vanderwerf
2026-02-06 94de71140be2d0c80bf6a2e03cb9381b37736ed5
inc/rest/routes/ContentTermsRoutes.php
@@ -1,17 +1,566 @@
<?php
namespace JVBase\rest\routes;
use JVBase\managers\queue\executors\ContentTermExecutor;
use JVBase\managers\queue\TypeConfig;
use JVBase\rest\Rest;
use JVBase\rest\Route;
use JVBase\rest\PermissionHandler;
use JVBase\utility\Features;
use JVBase\managers\CustomTable;
use WP_REST_Request;
use WP_REST_Response;
use Exception;
if (!defined('ABSPATH')) {
   exit;
}
/**
 * Generic routes for managing content taxonomies (shops, studios, etc.)
 *
 * Responsibilities:
 * - Update term settings/metadata
 * - Manage members (add/remove via history table)
 * - Manage ownership/management permissions
 * - Process membership requests (if verify_entry enabled)
 *
 * Note: Invitations handled by InvitationRoutes
 * Note: Approvals handled by ApprovalRoutes
 */
class ContentTermsRoutes extends Rest
{
   protected string $taxonomy;
   protected array $config;
   protected ?CustomTable $historyTable = null;
   protected ?CustomTable $requestsTable = null;
   public function __construct(string $taxonomy = '')
   {
      $this->taxonomy = jvbNoBase($taxonomy);
      if ($taxonomy && isset(JVB_TAXONOMY[$this->taxonomy])) {
         $this->config = JVB_TAXONOMY[$this->taxonomy];
         $this->cacheName = $this->taxonomy;
         parent::__construct();
         $this->setupTables();
         $this->setupCacheConnections();
      }
      add_action('init', [$this, 'registerContentTermsExecutors'], 5);
   }
   public function registerContentTermsExecutors():void
   {
      $registry = JVB()->queue()->registry();
      $executor = new ContentTermExecutor();
      $taxonomies = Features::getTypesWithFeature('is_content', 'taxonomy');
      foreach($taxonomies as $taxonomy) {
         $registry->register("{$taxonomy}_update", new TypeConfig(
            executor: $executor,
         ));
         if (Features::forTaxonomy($taxonomy)->has('track_changes')) {
            $registry->register("{$taxonomy}_member_add", new TypeConfig(
               executor: $executor
            ));
            $registry->register("{$taxonomy}_member_remove", new TypeConfig(
               executor: $executor
            ));
         }
      }
   }
   protected function setupTables(): void
   {
      $content = $this->config['for_content'] ?? [];
      if (Features::forTaxonomy($this->taxonomy)->has('track_changes') && !empty($content)) {
         foreach ($content as $contentType) {
            $tableName = "history_{$contentType}_{$this->taxonomy}";
            $this->historyTable = CustomTable::for($tableName);
            break; // Only need one table
         }
      }
      if (Features::forTaxonomy($this->taxonomy)->has('verify_entry') && !empty($content)) {
         foreach ($content as $contentType) {
            $tableName = "{$contentType}_{$this->taxonomy}_requests";
            $this->requestsTable = CustomTable::for($tableName);
            break;
         }
      }
   }
   protected function setupCacheConnections(): void
   {
      $this->cache
         ->connect('taxonomy')
         ->connect('user');
   }
   public function registerRoutes(): void
   {
      // TODO: Implement registerRoutes() method.
      if (!Features::forTaxonomy($this->taxonomy)->has('is_content')) {
         return;
      }
      $base = $this->taxonomy;
      // Update term settings
      Route::for("{$base}/:term_id/settings")
         ->post([$this, 'updateSettings'])
         ->args([
            'user' => 'int|required',
            'term_id' => 'int|required'
         ])
         ->auth(PermissionHandler::custom([$this, 'checkTermPermission']))
         ->rateLimit(10);
      // Member management (if track_changes enabled)
      if (Features::forTaxonomy($this->taxonomy)->has('track_changes')) {
         Route::for("{$base}/:term_id/members")
            ->get([$this, 'getMembers'])
            ->args([
               'term_id' => 'int|required',
               'status' => 'string|enum:active,inactive,all|default:active',
               'page' => 'int|default:1|min:1'
            ])
            ->auth('logged_in')
            ->rateLimit(30)
            ->post([$this, 'manageMember'])
            ->args([
               'user' => 'int|required',
               'term_id' => 'int|required',
               'target_user' => 'int|required',
               'action' => 'string|enum:add,remove|required'
            ])
            ->auth(PermissionHandler::custom([$this, 'checkTermPermission']))
            ->rateLimit(5);
      }
      // Membership requests (if verify_entry enabled)
      if (Features::forTaxonomy($this->taxonomy)->has('verify_entry')) {
         Route::for("{$base}/:term_id/requests")
            ->get([$this, 'getRequests'])
            ->args([
               'user' => 'int|required',
               'term_id' => 'int|required',
               'status' => 'string|enum:requested,accepted,rejected,all|default:requested',
               'page' => 'int|default:1|min:1'
            ])
            ->auth(PermissionHandler::custom([$this, 'checkTermPermission']))
            ->rateLimit(20);
         Route::for("{$base}/request")
            ->post([$this, 'handleRequest'])
            ->args([
               'user' => 'int|required',
               'term_id' => 'int|required',
               'action' => 'string|enum:create,accept,reject|required',
               'request_id' => 'int',
               'notes' => 'string'
            ])
            ->auth('verified')
            ->rateLimit(5);
      }
      // Ownership/management (if is_ownable enabled)
      if (Features::forTaxonomy($this->taxonomy)->has('is_ownable')) {
         Route::for("{$base}/:term_id/permissions")
            ->post([$this, 'updatePermissions'])
            ->args([
               'user' => 'int|required',
               'term_id' => 'int|required',
               'target_user' => 'int|required',
               'role' => 'string|enum:owner,manager|required',
               'grant' => 'bool|required'
            ])
            ->auth(PermissionHandler::custom([$this, 'checkOwnerPermission']))
            ->rateLimit(5);
      }
   }
   /**
    * Permission Callbacks
    */
   public function checkTermPermission(WP_REST_Request $request): bool
   {
      $userID = $request->get_param('user') ?? get_current_user_id();
      $termID = (int)$request->get_param('term_id');
      if (!$this->checkUser($userID) || !term_exists($termID, jvbCheckBase($this->taxonomy))) {
         return false;
      }
      return user_can($userID, 'manage_options') ||
         JVB()->roles()->isManager($userID, $termID);
   }
   public function checkOwnerPermission(WP_REST_Request $request): bool
   {
      $userID = $request->get_param('user') ?? get_current_user_id();
      $termID = (int)$request->get_param('term_id');
      if (!$this->checkUser($userID) || !term_exists($termID, jvbCheckBase($this->taxonomy))) {
         return false;
      }
      return user_can($userID, 'manage_options') ||
         JVB()->roles()->isOwner($userID, $termID);
   }
   /**
    * Update term settings
    */
   public function updateSettings(WP_REST_Request $request): WP_REST_Response
   {
      $termID = (int)$request->get_param('term_id');
      $userID = $request->get_param('user');
      $data = $request->get_params();
      unset($data['user'], $data['term_id']);
      // Queue the update
      $op = JVB()->queue()->add(
         "{$this->taxonomy}_update",
         $userID,
         array_merge(['term_id' => $termID], $data),
         [
            'priority' => 'high',
            'notification' => true
         ]
      );
      return $this->queued($op['operation_id']);
   }
   /**
    * Get term members
    */
   public function getMembers(WP_REST_Request $request): WP_REST_Response
   {
      $termID = (int)$request->get_param('term_id');
      $status = $request->get_param('status');
      $page = (int)$request->get_param('page');
      $cacheKey = $this->cache->generateKey(compact('termID', 'status', 'page'));
      return $this->cache->remember($cacheKey, function() use ($termID, $status, $page) {
         $perPage = 20;
         $offset = ($page - 1) * $perPage;
         $query = $this->historyTable->where(['term_id' => $termID]);
         if ($status === 'active') {
            $query = $query->where(['end_date' => null]);
         } elseif ($status === 'inactive') {
            $query = $query->whereNotNull('end_date');
         }
         $total = $query->countResults();
         $members = $query
            ->orderBy('is_primary', 'DESC')
            ->orderBy('created_at', 'ASC')
            ->limit($perPage, $offset)
            ->getResults(ARRAY_A);
         // Enrich with user data
         $members = array_map(function($member) {
            $user = get_userdata($member['user_id']);
            $member['display_name'] = $user ? $user->display_name : 'Unknown';
            $member['user_email'] = $user ? $user->user_email : '';
            return $member;
         }, $members);
         return $this->success([
            'members' => $members,
            'total' => $total,
            'pages' => ceil($total / $perPage),
            'page' => $page,
            'per_page' => $perPage
         ]);
      });
   }
   /**
    * Manage member (add/remove)
    */
   public function manageMember(WP_REST_Request $request): WP_REST_Response
   {
      $action = $request->get_param('action');
      $userID = $request->get_param('user');
      $termID = (int)$request->get_param('term_id');
      $targetUserID = (int)$request->get_param('target_user');
      if (!$this->checkUser($targetUserID)) {
         return $this->error('Invalid target user');
      }
      // Queue the operation
      $op = JVB()->queue()->add(
         "{$this->taxonomy}_member_{$action}",
         $userID,
         [
            'term_id' => $termID,
            'target_user' => $targetUserID
         ],
         ['priority' => 'high']
      );
      return $this->queued($op['operation_id']);
   }
   /**
    * Get membership requests
    */
   public function getRequests(WP_REST_Request $request): WP_REST_Response
   {
      $termID = (int)$request->get_param('term_id');
      $status = $request->get_param('status');
      $page = (int)$request->get_param('page');
      $cacheKey = $this->cache->generateKey(compact('termID', 'status', 'page'));
      return $this->cache->remember($cacheKey, function() use ($termID, $status, $page) {
         $perPage = 20;
         $offset = ($page - 1) * $perPage;
         $query = $this->requestsTable->where(['term_id' => $termID]);
         if ($status !== 'all') {
            $query = $query->where(['status' => $status]);
         }
         $total = $query->countResults();
         $requests = $query
            ->orderBy('created_date', 'DESC')
            ->limit($perPage, $offset)
            ->getResults(ARRAY_A);
         return $this->success([
            'requests' => $requests,
            'total' => $total,
            'pages' => ceil($total / $perPage),
            'page' => $page,
            'per_page' => $perPage
         ]);
      });
   }
   /**
    * Handle membership request (create/accept/reject)
    */
   public function handleRequest(WP_REST_Request $request): WP_REST_Response
   {
      $action = $request->get_param('action');
      $userID = $request->get_param('user');
      $termID = (int)$request->get_param('term_id');
      return match($action) {
         'create' => $this->createRequest($userID, $termID),
         'accept', 'reject' => $this->processRequest($request),
         default => $this->error('Invalid action')
      };
   }
   protected function createRequest(int $userID, int $termID): WP_REST_Response
   {
      if (!term_exists($termID, jvbCheckBase($this->taxonomy))) {
         return $this->error('Invalid ' . $this->taxonomy);
      }
      // Check if request already exists
      $existing = $this->requestsTable
         ->where([
            'user_id' => $userID,
            'term_id' => $termID
         ])
         ->first();
      if ($existing) {
         return $this->error('Request already exists');
      }
      $contentID = get_user_meta($userID, BASE . 'link', true);
      if (!$contentID) {
         return $this->error('User profile not found');
      }
      try {
         $requestID = $this->requestsTable->create([
            'user_id' => $userID,
            'content_id' => $contentID,
            'term_id' => $termID,
            'status' => 'requested',
            'created_date' => current_time('mysql')
         ]);
         if ($requestID) {
            $this->notifyTermManagers($termID, $userID, 'membership_request');
            $this->cache->flush();
            return $this->success([
               'message' => 'Request submitted successfully',
               'request_id' => $requestID
            ]);
         }
         return $this->error('Failed to create request');
      } catch (Exception $e) {
         $this->logError('createRequest', [
            'error' => $e->getMessage(),
            'user_id' => $userID,
            'term_id' => $termID
         ]);
         return $this->error('An error occurred');
      }
   }
   protected function processRequest(WP_REST_Request $request): WP_REST_Response
   {
      $action = $request->get_param('action');
      $requestID = (int)$request->get_param('request_id');
      $notes = $request->get_param('notes') ?? '';
      if (!$requestID) {
         return $this->error('Request ID required');
      }
      $requestData = $this->requestsTable
         ->where(['id' => $requestID])
         ->first(ARRAY_A);
      if (!$requestData) {
         return $this->error('Request not found');
      }
      $status = $action === 'accept' ? 'accepted' : 'rejected';
      try {
         $this->requestsTable->transaction(function($table) use ($requestID, $status, $notes, $requestData) {
            // Update request
            $table->where(['id' => $requestID])->updateResults([
               'status' => $status,
               'notes' => $notes,
               'updated_date' => current_time('mysql')
            ]);
            // If accepted, add member via action
            if ($status === 'accepted') {
               do_action(
                  BASE . 'add_user_to_term',
                  $requestData['user_id'],
                  $requestData['term_id'],
                  $this->taxonomy,
                  'member'
               );
            }
         });
         // Notify user
         JVB()->notification()->addNotification(
            $requestData['user_id'],
            'request_' . $status,
            [
               'term_id' => $requestData['term_id'],
               'taxonomy' => $this->taxonomy,
               'notes' => $notes
            ]
         );
         $this->cache->flush();
         return $this->success(['message' => 'Request ' . $status]);
      } catch (Exception $e) {
         $this->logError('processRequest', [
            'error' => $e->getMessage(),
            'request_id' => $requestID,
            'action' => $action
         ]);
         return $this->error('Failed to process request');
      }
   }
   /**
    * Update ownership/management permissions
    */
   public function updatePermissions(WP_REST_Request $request): WP_REST_Response
   {
      $termID = (int)$request->get_param('term_id');
      $targetUserID = (int)$request->get_param('target_user');
      $role = $request->get_param('role');
      $grant = (bool)$request->get_param('grant');
      if (!$this->checkUser($targetUserID)) {
         return $this->error('Invalid target user');
      }
      $roleManager = JVB()->roles();
      try {
         $result = match($role) {
            'owner' => $grant
               ? $roleManager->grantOwnership($targetUserID, $termID, $this->taxonomy)
               : $roleManager->revokeOwnership($targetUserID, $termID, $this->taxonomy),
            'manager' => $grant
               ? $roleManager->grantManagement($targetUserID, $termID, $this->taxonomy)
               : $roleManager->revokeManagement($targetUserID, $termID, $this->taxonomy),
            default => false
         };
         if ($result) {
            $this->cache->flush();
            return $this->success([
               'message' => ucfirst($role) . ' permissions ' . ($grant ? 'granted' : 'revoked')
            ]);
         }
         return $this->error('Failed to update permissions');
      } catch (Exception $e) {
         $this->logError('updatePermissions', [
            'error' => $e->getMessage(),
            'term_id' => $termID,
            'target_user' => $targetUserID,
            'role' => $role
         ]);
         return $this->error('An error occurred');
      }
   }
   /**
    * Notify term managers
    */
   protected function notifyTermManagers(int $termID, int $actingUserID, string $notificationType): void
   {
      $roleManager = JVB()->roles();
      $managers = $roleManager->getManagedTerms($actingUserID, $this->taxonomy);
      $term = get_term($termID, jvbCheckBase($this->taxonomy));
      foreach ($managers as $managerID) {
         JVB()->notification()->addNotification(
            $managerID,
            $notificationType,
            [
               'user_id' => $actingUserID,
               'term_id' => $termID,
               'term_name' => $term->name ?? 'Unknown',
               'taxonomy' => $this->taxonomy
            ]
         );
      }
   }
}