Jake Vanderwerf
8 days ago 3b83905603d44b1a08f8b2b36a605808ce686ad6
inc/rest/routes/TermRoutes.php
@@ -1,10 +1,11 @@
<?php
namespace JVBase\rest\routes;
use JVBase\JVB;
use JVBase\rest\RestRouteManager;
use JVBase\managers\TaxonomyRelationships;
use JVBase\registrar\Registrar;
use JVBase\rest\Rest;
use JVBase\managers\UserTermsManager;
use JVBase\rest\Route;
use JVBase\base\Site;
use WP_REST_Request;
use WP_REST_Response;
use Exception;
@@ -12,17 +13,21 @@
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}
class TermRoutes extends RestRouteManager
class TermRoutes extends Rest
{
    protected object $term_index_manager;
    protected int $per_page;
    public function __construct()
    {
        $this->cache_name = 'terms';
        $this->cacheName = 'terms';
        parent::__construct();
//    $this->cache->invalidateGroup('terms');
      if (JVB_TESTING) {
         $this->cache->flush();
      }
      $this->cache->connect('taxonomy', true);
        $this->per_page = 20;
        add_action('edited_term', [$this, 'deleteTermPath']);
        add_action('wp_login', [$this, 'clearUserTaxonomyCache'], 10, 2);
    }
@@ -33,60 +38,31 @@
     */
    public function registerRoutes():void
    {
        register_rest_route($this->namespace, '/terms', [
            [
                'methods'   => 'GET',
                'callback'  => [$this, 'handleTermSelectionRequest'],
                'permission_callback'   => [$this, 'checkPermission'],
                'args' => [
                    'page'  => [
                        'required'  => true,
                        'type'      => 'integer',
                        'default'   => 1,
                    ],
                    'taxonomy' => [
                        'required' => true,
                        'type' => 'string'
                    ],
                    'search' => [
                        'required' => false,
                        'type' => 'string'
                    ],
                    'parent' => [
                        'required' => false,
                        'type' => 'integer',
                        'default'   => 0
                    ]
                ]
            ],
            [
                'methods'   => 'POST',
                'callback' => [$this, 'createTermRequest'],
                'permission_callback' => [$this, 'checkPermission'],
                'args' => [
                    'taxonomy' => [
                        'required' => true,
                        'type' => 'string'
                    ],
                    'name' => [
                        'required' => true,
                        'type' => 'string'
                    ],
                    'parent' => [
                        'required' => false,
                        'type' => 'integer',
                        'default' => 0
                    ]
                ]
            ]
        ]);
      Route::for('terms')
         ->get([$this, 'handleTermSelectionRequest'])
         ->auth('public')
         ->rateLimit()
         ->args([
            'page'      => 'int|required',
            'taxonomy'  => 'string|required',
            'search' => 'string',
            'parent' => 'int'
         ])
         ->post([$this, 'createTermRequest'])
         ->auth('isVerified')
         ->rateLimit(30)
         ->args([
            'taxonomy'  => 'string|required',
            'name'      => 'string|required',
            'parent' => 'int|default:0',
         ])
         ->register();
        //TODO: Just wrap this up to the normal GET request
        register_rest_route($this->namespace, '/terms/check', [
            'methods' => 'GET',
            'callback' => [$this, 'getTermDetails'],
            'permission_callback' => [$this, 'checkPermission'],
        ]);
      Route::for('terms/check')
         ->get([$this,'getTermDetails'])
         ->auth('public')
         ->rateLimit()
         ->register();
    }
    /**
@@ -100,22 +76,19 @@
        $term = get_term($term_id);
        if (is_wp_error($term)) {
            return new WP_REST_Response([
                'success'   => false,
                'message'   => $term
            ]);
         return $this->error('No term found');
        }
        $data = [
            'id' => $term->term_id,
            'name' => $term->name,
            'name' => html_entity_decode($term->name),
            'slug' => $term->slug,
            'taxonomy' => $term->taxonomy,
        ];
        // Add relationship data if requested
        if ($request->get_param('include_relationships')) {
            $relationship_manager = new TaxonomyRelationships();
            $relationship_manager = JVB()->termRelationships();
            $related_taxonomies = $request->get_param('related_taxonomies') ?: ['jvb_style', 'jvb_theme', 'jvb_city'];
            $data['relationships'] = [];
@@ -130,7 +103,7 @@
                        $term = get_term($rel->related_term_id, $rel->related_taxonomy);
                        return [
                            'id' => $rel->related_term_id,
                            'name' => $term ? $term->name : 'Unknown',
                            'name' => $term ? html_entity_decode($term->name) : 'Unknown',
                            'count' => $rel->relationship_count
                        ];
                    }, $relationships);
@@ -138,7 +111,7 @@
            }
        }
        return new WP_REST_Response($data);
        return $this->success($data);
    }
    /**
@@ -146,31 +119,33 @@
     *
     * @return WP_REST_Response
     */
    public function getTermDetails(WP_REST_Request $request):WP_REST_Response
    {
        $data = $request->get_params();
      // Collect all taxonomies being queried
   public function getTermDetails(WP_REST_Request $request): WP_REST_Response
   {
      $data = $request->get_params();
      $taxonomies = array_keys($data);
      // Check HTTP cache headers
      $cache_check = $this->checkHeaders($request, $taxonomies);
      $cache_check = $this->checkHeaders($request, $this->cache->generateKey(['termDetails' => $data]));
      if ($cache_check) {
         return $cache_check;
      }
        $terms = [];
        foreach ($data as $tax => $IDs) {
            $args = [
                'taxonomy'  => BASE.$tax,
                'include'   => $IDs
            ];
            $terms[$tax] = $this->formatTerms($args, BASE.$tax);
        }
        $response = new WP_REST_Response([
            'items' => $terms,
        ]);
      return $this->addCacheHeaders($response);
    }
      $terms = $this->cache->remember(
         $this->cache->generateKey(['termDetails' => $data]),
         function() use ($data) {
            $result = [];
            foreach ($data as $tax => $IDs) {
               $args = [
                  'taxonomy' => BASE . $tax,
                  'include' => $IDs
               ];
               $result[$tax] = $this->formatTerms($args, BASE . $tax);
            }
            return $result;
         }
      );
      return $this->addCacheHeaders($this->success(['items' => $terms]));
   }
    /**
     * @param WP_REST_Request $request
@@ -180,9 +155,7 @@
    public function handleTermSelectionRequest(WP_REST_Request $request):WP_REST_Response
    {
      $data = $request->get_params();
      $taxonomy = jvbCheckBase($data['taxonomy']);
      error_log('Term Request Data for '.$taxonomy.': '.print_r($data, true));
      $taxonomy = sanitize_text_field($data['taxonomy'])??'';
      // Check HTTP cache headers
      $cache_check = $this->checkHeaders($request, $taxonomy);
      if ($cache_check) {
@@ -190,16 +163,22 @@
         return $cache_check;
      }
      // Handle batch request (multiple taxonomies)
      if (str_contains($taxonomy, ',')) {
         return $this->handleBatchTermRequest($taxonomy, $data, $request);
      }
      $taxonomy = jvbCheckBase($taxonomy);
      if (array_key_exists('termIDs', $data)) {
         $args = [
            'taxonomy'  => $taxonomy,
            'include'   => $data['termIDs'],
            'hide_empty'   => false,
            'hide_empty'   => true,
         ];
         $key = $this->cache->generateKey($args);
         $cached = $this->cache->get($key);
         if ($cached) {
            $response = new WP_REST_Response($cached);
            $response = $this->success($cached);
            return $this->addCacheHeaders($response);
         }
@@ -208,14 +187,14 @@
            'items'  => $formatted
         ];
         $this->cache->set($key, $response);
         $response = new WP_REST_Response($response);
         $response = $this->success($response);
         return $this->addCacheHeaders($response);
      }
      if (array_key_exists('content', $data)) {
         // If content_type is provided, use the specialized endpoint
         $content_type = $request->get_param('content');
         global $feed_types;
         if (taxIsJVBContentTax($content_type)) {
         $registrar = Registrar::getInstance($content_type);
         if ($registrar->hasFeature('is_content')) {
            $response = $this->getTermsForContentType($request);
            return $this->addCacheHeaders($response);
         }
@@ -223,21 +202,13 @@
        $taxonomy = BASE.$request->get_param('taxonomy');
        $search = $request->get_param('search');
        $parent = (int)$request->get_param('parent');
        $page = max(1, (int)$request->get_param('page'));
        $per_page = max(20, (int)$request->get_param('per_page'));
      $parent = (int)$data['parent']??0;
      $page = max(1, (int)($data['page']??1));
      $per_page = 25;
        if (!taxonomy_exists($taxonomy)) {
            return new WP_REST_Response([
                'items' => [],
                'pagination' => [
                    'page' => 1,
                    'per_page' => $per_page,
                    'total_pages' => 0,
                    'total_terms' => 0,
                    'has_more' => false
                ]
            ]);
         return $this->emptyResult();
        }
        $tax_obj = get_taxonomy($taxonomy);
@@ -253,7 +224,7 @@
        // Get terms for current level with child count
        $args = [
            'taxonomy' => $taxonomy,
            'hide_empty' => false,
            'hide_empty' => true,
            'parent' => $parent,
            'number' => $per_page,
         'orderby'=> 'name',
@@ -265,21 +236,12 @@
        if ($request->get_param('main_context') && in_array(jvbCheckBase(json_decode($request->get_param('main_context'), true)['context']), jvbUserTypes())) {
            $main_context = json_decode($request->get_param('main_context'), true);
            $userID = get_post_meta($main_context['id'], BASE.'link', true);
            $userID = get_post_meta($main_context['id'], BASE.'profile_link', true);
            $manager = new UserTermsManager();
            $related = $manager->getUserTermIDs($userID, $taxonomy);
            $related = $manager->fetchUserTerms($userID, $taxonomy);
            if (empty($related)) {
                $response = new WP_REST_Response([
                    'items' => [],
                    'pagination' => [
                        'page' => 1,
                        'per_page' => $per_page,
                        'total_pages' => 0,
                        'total_terms' => 0,
                        'has_more' => false
                    ]
                ]);
                $response = $this->emptyResult();
            return $this->addCacheHeaders($response);
            }
@@ -289,20 +251,11 @@
            $main_context = json_decode($request->get_param('main_context'), true);
            $thisTaxonomy = str_replace('taxonomy:', '', $main_context['context']);
            $ID = (int)$main_context['id'];
            $manager = new TaxonomyRelationships();
            $manager = JVB()->termRelationships();
            $related = $manager->getRelatedTerms($ID, BASE.$request->get_param('taxonomy'));
            if (empty($related)) {
                $response = new WP_REST_Response([
                    'items' => [],
                    'pagination' => [
                        'page' => 1,
                        'per_page' => $per_page,
                        'total_pages' => 0,
                        'total_terms' => 0,
                        'has_more' => false
                    ]
                ]);
                $response = $this->emptyResult();
            return $this->addCacheHeaders($response);
            }
            $args['tax_query'] = [
@@ -316,7 +269,7 @@
            $match = $request->get_param('match') ?? 'any';
            $context = json_decode($request['context'], true);
            $relationshipManager = new TaxonomyRelationships();
            $relationshipManager = JVB()->termRelationships();
            // Prepare array to collect term IDs that match the context
            $related_term_ids = [];
@@ -352,16 +305,7 @@
                $args['include'] = $related_term_ids;
            } else {
                // No related terms found, return empty result
                $response =  new WP_REST_Response([
                    'items' => [],
                    'pagination' => [
                        'page' => 1,
                        'per_page' => $per_page,
                        'total_pages' => 0,
                        'total_terms' => 0,
                        'has_more' => false
                    ]
                ]);
                $response = $this->emptyResult();
            return $this->addCacheHeaders($response);
            }
@@ -371,9 +315,9 @@
        $key = $this->cache->generateKey($args);
        $cache = $this->cache->get($key);
      $cache = false;
        if ($cache) {
            $response = new WP_ReST_Response($cache);
            $response = $this->success($cache);
         return $this->addCacheHeaders($response);
        }
@@ -396,138 +340,176 @@
                'page' => $page,
                'per_page' => $per_page,
                'total_pages' => $total_pages,
                'total_terms' => (int)$total_terms,
                'has_more' => $has_more
            ]
                'total_terms' => (int)$total_terms
            ],
         'has_more' => $has_more
        ];
        $this->cache->set($key, $response);
        $response = new WP_REST_Response($response);
        $response = $this->success($response);
      return $this->addCacheHeaders($response);
    }
   protected function handleBatchTermRequest(string $taxonomy, array $data, WP_REST_Request $request):WP_REST_Response
   {
      $taxonomies = array_map('trim', explode(',', $taxonomy));
      $all_terms = [];
      $parent = (int)$data['parent']??0;
      $page = max(1, (int)($data['page']??1));
      $per_page = 25;
      $mainArgs = [
         'hide_empty'=> false,
         'parent' => $parent,
         'number' => $per_page,
         'orderby'   => 'name',
         'offset' => ($page -1) * $per_page,
      ];
      foreach ($taxonomies as $taxonomy) {
         if (!taxonomy_exists(BASE.$taxonomy)) {
            continue;
         }
         $args = $mainArgs;
         $args['taxonomy'] = BASE.$taxonomy;
         $all_terms = array_merge($all_terms, $this->formatTerms($args, $taxonomy));
      }
      $response = [
         'items'  => $all_terms,
         'pagination'=> [
            'page' => $page,
            'per_page'=> $per_page
         ],
         'has_more' => true,
      ];
      $response = $this->success($response);
      return $this->addCacheHeaders($response);
   }
    /**
     * @param array $args
     * @param string $taxonomy
     *
     * @return array
     */
    protected function formatTerms(array $args, string $taxonomy):array
    {
        $terms = get_terms($args);
   protected function formatTerms(array $args, string $taxonomy): array
   {
      return $this->cache->remember(
         $this->cache->generateKey($args),
         function() use ($args, $taxonomy) {
            $terms = get_terms($args);
        if (is_wp_error($terms)) {
            return [];
        }
            if (is_wp_error($terms)) {
               return [];
            }
            $formatted_terms = [];
            foreach ($terms as $term) {
               $formatted_terms[] = $this->formatSingleTerm($term, $taxonomy, true);
            }
            return $formatted_terms;
         }
      );
   }
        $formatted_terms = [];
        foreach ($terms as $term) {
            // Check for children explicitly
            $children_args = [
                'taxonomy' => $taxonomy,
                'parent' => $term->term_id,
                'fields' => 'count',
                'hide_empty' => false
            ];
            $count = wp_count_terms($children_args);
            $has_children = !is_wp_error($count) && $count > 0;
   /**
    * Format a single term with caching
    *
    * @param object $term WP_Term object
    * @param string $taxonomy Full taxonomy name
    *
    * @return array Formatted term data
    */
   protected function formatSingleTerm(object $term, string $taxonomy): array
   {
      $cache_key = "{$term->term_id}_{$taxonomy}";
            $formatted_terms[] = [
                'id' => $term->term_id,
                'name' => $term->name,
            'slug'   => $term->slug,
                'parent' => $term->parent,
                'path' => $this->getTermPath($term->term_id, $term->name, $taxonomy),
                'hasChildren' => $has_children,
            'taxonomy'  => $term->taxonomy,
            'count'     => $term->count,
            ];
        }
      return $this->cache->remember($cache_key, function() use ($term, $taxonomy) {
         $data = [
            'id' => $term->term_id,
            'name' => html_entity_decode($term->name),
            'slug' => $term->slug,
            'parent' => $term->parent,
            'path' => $this->getTermPath($term->term_id, $term->name, $taxonomy),
            'taxonomy' => jvbNoBase($term->taxonomy),
            'count' => $term->count,
         ];
        return $formatted_terms;
    }
         $children_args = [
            'taxonomy' => $taxonomy,
            'parent' => $term->term_id,
            'fields' => 'count',
            'hide_empty' => false
         ];
         $count = wp_count_terms($children_args);
         $data['hasChildren'] = !is_wp_error($count) && $count > 0;
         return $data;
      });
   }
    /**
     * @param WP_REST_Request $request
     *
     * @return WP_REST_Response
     */
    public function handleTermSearch(WP_REST_Request $request):WP_REST_Response
    {
        $taxonomy = BASE.$request->get_param('taxonomy');
        $search = $request->get_param('search');
        $page = $request->get_param('page') ?? 1;
        $per_page = $request->get_param('per_page') ?? 20;
   public function handleTermSearch(WP_REST_Request $request): WP_REST_Response
   {
      $taxonomy = BASE . $request->get_param('taxonomy');
      $search = $request->get_param('search');
      $page = $request->get_param('page') ?? 1;
      $per_page = $request->get_param('per_page') ?? 20;
        // When searching, we want to search across all terms regardless of hierarchy
        $args = [
            'taxonomy' => $taxonomy,
            'hide_empty' => false,
            'search' => $search,
            'search_columns' => ['name', 'slug'],
            'fields' => 'all',
            'number' => $per_page,
            'offset' => ($page - 1) * $per_page,
        ];
      $args = [
         'taxonomy' => $taxonomy,
         'hide_empty' => true,
         'search' => $search,
         'search_columns' => ['name', 'slug'],
         'fields' => 'all',
         'number' => $per_page,
         'offset' => ($page - 1) * $per_page,
      ];
        $key = $this->cache->generateKey($args);
        $cache = $this->cache->get($key);
        if ($cache) {
            return new WP_REST_Response($cache);
        }
      $data = $this->cache->remember(
         $this->cache->generateKey($args),
         function() use ($args, $taxonomy, $page, $per_page) {
            $terms = get_terms($args);
        $terms = get_terms($args);
            if (is_wp_error($terms)) {
               return $this->emptyResult($page, $per_page);
            }
        if (is_wp_error($terms)) {
            return new WP_REST_Response([
                'items' => [],
                'pagination' => [
                    'page' => 0,
                    'per_page' => 20,
                    'total_pages' => 0,
                    'total_terms' => 0,
                    'has_more' => false
                ]
            ]);
        }
            $formatted_terms = array_map(
               fn($term) => $this->formatSingleTerm($term, $taxonomy),
               $terms
            );
        // Get total count for pagination
        $count_args = array_merge($args, ['fields' => 'count']);
        $total_terms = wp_count_terms($count_args);
            $count_args = array_merge($args, ['fields' => 'count']);
            $total_terms = wp_count_terms($count_args);
            $total_pages = ceil($total_terms / $per_page);
        $formatted_terms = [];
        foreach ($terms as $term) {
            $formatted_terms[$term->term_id] = [
                'id'            => $term->term_id,
                'name'          => $term->name,
                'parent'        => $term->parent,
                'path'          => $this->getTermPath($term->term_id, $term->name, $taxonomy),
                'hasChildren'   => (bool)$term->has_children,
                'count'         => (int)$term->count
            ];
        }
            return [
               'items' => $formatted_terms,
               'pagination' => [
                  'page' => (int)$page,
                  'per_page' => (int)$per_page,
                  'total_pages' => $total_pages,
                  'total_terms' => (int)$total_terms
               ],
               'has_more' => $page < $total_pages
            ];
         }
      );
        // Calculate pagination info
        $total_pages = ceil($total_terms / $per_page);
        $has_more = $page < $total_pages;
        $response = [
            'items' => $formatted_terms,
            'pagination' => [
                'page' => (int)$page,
                'per_page' => (int)$per_page,
                'total_pages' => $total_pages,
                'total_terms' => (int)$total_terms,
                'has_more' => $has_more
            ]
        ];
        $this->cache->set($key, $response);
        return new WP_REST_Response($response);
    }
      return $this->addCacheHeaders($this->success($data));
   }
    /**
     * @param int $termID
@@ -601,7 +583,7 @@
     */
    public function getTermsForContentType(WP_REST_Request $request):WP_REST_Response
    {
        $manager = new TaxonomyRelationships();
        $manager = JVB()->termRelationships();
        $content_type = BASE . $request->get_param('content');
        $taxonomy = BASE . $request->get_param('taxonomy');
        $search = $request->get_param('search');
@@ -621,7 +603,8 @@
        $cache = $this->cache->get($cache_key);
        // Try cache first
        if ($cache !== false) {
            return new WP_REST_Response($cache);
         $response = $this->success($cache);
            return $this->addCacheHeaders($response);
        }
        try {
@@ -698,15 +681,12 @@
            $formatted_terms = [];
            $is_hierarchical = is_taxonomy_hierarchical($taxonomy);
            foreach ($terms as $term) {
                $formatted_terms[$term->term_id] = [
                    'id' => $term->term_id,
                    'name' => $term->name,
                    'count' => $term->count,
                    'path' => $this->getTermPath($term->term_id, $term->name, $taxonomy),
                    'relationship_strength' => $term->relationship_count ?? 0
                ];
            }
         foreach ($terms as $term) {
            $formatted = $this->formatSingleTerm($term, $taxonomy, false);
            // Add relationship strength which is unique to this method
            $formatted['relationship_strength'] = $term->relationship_count ?? 0;
            $formatted_terms[] = $formatted;
         }
            // Build response
            $total_pages = ceil($total / $per_page);
@@ -716,166 +696,61 @@
                    'page'      => (int)$page,
                    'per_page'  => (int)$per_page,
                    'total_terms'=> $total,
                    'total_pages'=> $total_pages,
                    'has_more'  => $page < $total_pages
                ]
                    'total_pages'=> $total_pages
                ],
            'has_more'  => $page < $total_pages
            ];
            // Cache results
            $this->cache->set($cache_key, $results);
            return new WP_REST_Response($results);
         $response = $this->success($results);
            return $this->addCacheHeaders($response);
        } catch (Exception $e) {
            return new WP_REST_Response([
                'success'   => false,
                'message'   => $e->getMessage()
            ]);
         return $this->error('Error getting terms for content: '.$e->getMessage(), 'get_terms_for_content');
        }
    }
    /**
     * @param WP_REST_Request $request
     *
     * @return WP_REST_Response
     */
    public function handleTermsRequest(WP_REST_Request $request):WP_REST_Response
    {
        $taxonomy = BASE.$request->get_param('taxonomy');
        $search = $request->get_param('search');
        $page = max(1, intval($request->get_param('page')));
        $per_page = $request->get_param('per_page') ?: $this->per_page;
   public function createTermRequest(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = get_current_user_id();
      $taxonomy = $request->get_param('taxonomy');
      $name = sanitize_text_field($request->get_param('name'));
      $parent = (int)$request->get_param('parent') ?: 0;
        // Create cache key
        $cache_key = "terms_{$taxonomy}_" . md5("{$search}_{$page}_{$per_page}");
        $cache = $this->cache->get($cache_key);
        if ($cache) {
            return new WP_REST_Response($cache);
        }
      try {
         $existing = term_exists($name, jvbCheckBase($taxonomy), $parent);
        try {
            // Build query args
            $args = [
                'taxonomy' => $taxonomy,
                'hide_empty' => false,
                'orderby' => $search ? 'name' : 'count',
                'order' => $search ? 'ASC' : 'DESC',
                'number' => $per_page,
                'offset' => ($page - 1) * $per_page,
                'fields' => 'all'
            ];
         if ($existing) {
            $term = get_term($existing['term_id'], jvbCheckBase($taxonomy));
            return $this->success(['message' => 'Term already exists', 'term' => [
               'id' => $term->term_id,
               'name' => html_entity_decode($term->name),
               'path' => $this->getTermPath($term->term_id, $term->name, $taxonomy)
            ]]);
         }
            // Add search if provided
            if ($search) {
                $args['search'] = $search;
            }
            // Get terms
            $terms = get_terms($args);
            if (is_wp_error($terms)) {
                return new WP_REST_Response([
                    'items' => [],
                    'pagination' => [
                        'page' => 0,
                        'per_page' => 20,
                        'total_pages' => 0,
                        'total_terms' => 0,
                        'has_more' => 0
                    ]
                ]);
            }
            // Check if taxonomy is hierarchical
            $is_hierarchical = is_taxonomy_hierarchical($taxonomy);
            // Format terms
            $formatted_terms = [];
            foreach ($terms as $term) {
                $formatted_terms[$term->term_id] = [
                    'id' => $term->term_id,
                    'name' => $term->name,
                    'count' => $term->count,
                    'parent' => $term->parent,
                    'path' => $this->getTermPath($term->term_id, $term->name, $taxonomy)
                ];
            }
            // Get total for pagination
            $total_args = array_merge($args, ['fields' => 'count', 'number' => '']);
            $total = wp_count_terms($taxonomy, $total_args);
            $total_pages = ceil($total / $per_page);
            $results = [
                'items' => $formatted_terms,
                'pagination' => [
                    'page' => (int)$page,
                    'per_page' => (int)$per_page,
                    'total_pages' => $total_pages,
                    'total_terms' => (int)$total,
                    'has_more' => $page < $total_pages
                ]
            ];
            // Cache results
            $this->cache->set($cache_key, $results);
            return new WP_REST_Response($results);
        } catch (Exception $e) {
            return new WP_REST_Response([
                'success'       => false,
                'message'       =>  $e->getMessage()
            ]);
        }
    }
    /**
     * @param WP_REST_Request $request
     *
     * @return WP_REST_Response
     */
    public function createTermRequest(WP_REST_Request $request):WP_REST_Response
    {
        $user_id = get_current_user_id();
        $taxonomy = $request->get_param('taxonomy');
        $name = sanitize_text_field($request->get_param('name'));
        $parent = (int)$request->get_param('parent') ?: 0;
        try {
            // Check if term already exists
            $existing = term_exists($name, jvbCheckBase($taxonomy), $parent);
            if ($existing) {
                $term = get_term($existing['term_id'], jvbCheckBase($taxonomy));
                return new WP_REST_Response([
                    'success' => false,
                    'message' => 'Term already exists',
                    'term' => [
                        'id' => $term->term_id,
                        'name' => $term->name,
                        'path' => $this->getTermPath($term->term_id, $term->name, $taxonomy)
                    ]
                ]);
            }
         if (jvbSiteHasTermApproval()) {
            // Get approval routes instance
         $membership = Site::membership();
         if ($membership && $membership->has('term_approval')) {
            $approval_routes = JVB()->routes('approvals');
            // Create approval request
            $request_id = $approval_routes->createTermApprovalRequest(
               $user_id,
               $taxonomy,
               sanitize_title($name),
               absint($parent)
            );
            if (!$request_id) {
               throw new Exception('Failed to create approval request');
            }
            $return = [
               'success' => true,
            return $this->success([
               'message' => 'Term suggestion submitted for approval',
               'term' => [
                  'id' => 'pending_' . $request_id,
@@ -883,48 +758,50 @@
                  'pending' => true,
                  'request_id' => $request_id
               ]
            ];
         } else {
            $termID = wp_insert_term(
               $name,
               jvbCheckBase($taxonomy),
               [
                  'parent' => absint($parent??0)
               ]
            );
            if (is_wp_error($termID)) {
               throw new Exception('Failed to create new term');
            }
            $return = [
               'success'   => true,
               'message'   => $name.' created successfully',
               'term'      => [
                  'id'  => $termID['term_id'],
                  'name'   => $name,
                  'path'   => $this->getTermPath($termID['term_id'], $name, $taxonomy)
               ]
            ];
            ], 202); // 202 Accepted for pending approval
         }
            return new WP_REST_Response($return);
         $termID = wp_insert_term(
            $name,
            jvbCheckBase($taxonomy),
            ['parent' => absint($parent)]
         );
        } catch (Exception $e) {
            JVB()->error()->log(
                'terms',
                'Term creation failed: ' . $e->getMessage(),
                [
                    'user_id' => $user_id,
                    'taxonomy' => $taxonomy,
                    'name' => $name
                ]
            );
         if (is_wp_error($termID)) {
            throw new Exception($termID->get_error_message());
         }
            return new WP_REST_Response([
                'success' => false,
                'message' => $e->getMessage()
            ], 500);
        }
    }
         return $this->success([
            'message' => $name . ' created successfully',
            'term' => [
               'id' => $termID['term_id'],
               'name' => $name,
               'path' => $this->getTermPath($termID['term_id'], $name, $taxonomy)
            ]
         ], 201); // 201 Created
      } catch (Exception $e) {
         JVB()->error()->log(
            'terms',
            'Term creation failed: ' . $e->getMessage(),
            ['user_id' => $user_id, 'taxonomy' => $taxonomy, 'name' => $name]
         );
         return $this->error($e->getMessage(), 'term_creation_failed', 500);
      }
   }
   protected function emptyResult(int $page = 1, int $per_page = 20):WP_REST_Response
   {
      return $this->success([
         'items' => [],
         'pagination' => [
            'page' => $page,
            'per_page' => $per_page,
            'total_pages' => 0,
            'total_terms' => 0
         ],
         'has_more' => false
      ]);
   }
}