Jake Vanderwerf
2026-02-04 2127b1bdd73ecd2423e443992da4b442f5a3c1a3
inc/rest/routes/ApprovalRoutes.php
@@ -2,11 +2,12 @@
namespace JVBase\rest\routes;
use JVBase\JVB;
use JVBase\rest\RestRouteManager;
use JVBase\managers\CacheManager;
use JVBase\managers\CustomTable;
use JVBase\rest\PermissionHandler;
use JVBase\rest\Rest;
use JVBase\rest\Route;
use JVBase\rest\Response;
use JVBase\utility\Features;
use WP_User;
use WP_REST_Request;
use WP_REST_Response;
use Exception;
@@ -15,20 +16,18 @@
    exit; // Exit if accessed directly
}
class ApprovalRoutes extends RestRouteManager
class ApprovalRoutes extends Rest
{
    protected array $userTypes;
    protected array $termTypes;
    protected array $allTypes;
   protected array $requestTables;
   protected array $voteTables;
    protected int $expiryDays = 7;
   protected bool $hasMemberApproval = false;
    public function __construct()
    {
        $this->cache_name = 'approvals';
        $this->cacheName = 'approvals';
      $this->hasMemberApproval = Features::forMembership()->has('member_verified');
        parent::__construct();
@@ -63,37 +62,28 @@
    public function registerRoutes():void
    {
        register_rest_route($this->namespace, '/approvals', [
            [
                'methods'             => 'GET',
                'callback'            => [ $this, 'getApprovals' ],
                'permission_callback' => [ $this, 'checkPermission' ]
            ],
            [
                'methods'             => 'POST',
                'callback'            => [ $this, 'handleApprovalAction' ],
                'permission_callback' => [ $this, 'checkPermission' ]
            ]
        ]);
      Route::for('approvals')
         ->get([$this, 'getApprovals'])
         ->args([
            'user' => 'integer|required',
            'type' => 'string',
            'status' => 'string|enum:pending,approved,rejected,expired',
         ])
         ->auth(PermissionHandler::combine(['user', 'verified']))
         ->rateLimit(30)
         ->post([$this, 'handleAction'])
         ->args([
            'user' => 'integer|required',
            'request_id' => 'integer|required',
            'action' => 'string|required|enum:approve,reject',
            'type' => 'string|required',
            'notes' => 'string',
         ])
         ->auth(PermissionHandler::combine(['user', 'verified']))
         ->rateLimit(3);
    }
    /**
     * @param WP_REST_Request $request The REST request
     *
     * @return bool
     */
    public function checkPermission(WP_REST_Request $request):bool
    {
        $userID = get_current_user_id();
        if (!user_can($userID, 'skip_moderation')) {
            return false;
        }
        return parent::checkPermission($request);
    }
    /**
     * Handler for user registration
     *
     * @param int $user_id New user ID
@@ -101,24 +91,18 @@
     *
     * @return void
     */
    public function handleNewUserRegistration(int $user_id, object $user):void
    {
   public function handleNewUserRegistration(int $user_id, object $user): void
   {
      $intersect = array_intersect(
         array_map(
            function ($role) {
               return BASE.$role;
            },
            $this->userTypes
         ),
         array_map(fn($role) => BASE.$role, $this->userTypes),
         (array) $user->roles
      );
        if (!empty($intersect)) {
            // Mark as unverified initially
            $user->add_cap('skip_moderation', false);
            // Create approval request
            $this->createArtistApprovalRequest($user_id);
        }
    }
      if (!empty($intersect)) {
         $user->add_cap('skip_moderation', false);
         $this->createArtistApprovalRequest($user_id);
      }
   }
    /**
@@ -126,216 +110,170 @@
     *
     * @return WP_REST_Response
     */
    public function handleApprovalAction(WP_REST_Request $request):WP_REST_Response
    public function handleAction(WP_REST_Request $request):WP_REST_Response
    {
        $data       = $request->get_params();
        $request_id = $data['request_id'] ?? 0;
        $user_id    = (array_key_exists('user', $data) &&
                       is_numeric($data['user'])) ?
                        (int) $data['user'] : get_current_user_id();
        $action     = (array_key_exists('action', $data) && in_array($data['action'], [
                'approve',
                'reject'
            ])) ? $data['action'] : false;
      $data = $request->get_params();
      $request_id = absint($data['request_id']);
      $user_id = absint($data['user']);
      $action = sanitize_text_field($data['action']);
      $type = sanitize_text_field($data['type']);
      $notes = sanitize_text_field($data['notes'] ?? '');
        $type = (array_key_exists('type', $data) &&
                 in_array($data['type'], $this->allTypes)) ?
            $data['type'] :
            false;
        $notes = (array_key_exists('notes', $data)) ? sanitize_text_field($data['notes']) : '';
      if (!in_array($type, $this->allTypes)) {
         return Response::validationError(['message' => 'Invalid type']);
      }
        if ($action && $request_id !== 0 && $type) {
            $result = $this->handleVote($type, $action, $request_id, $user_id, $notes);
            return new WP_REST_Response([
                'success' => $result,
                'message' => $result ? 'Vote recorded successfully' : 'Failed to record vote'
            ], $result ? 200 : 500);
        }
        return new WP_REST_Response([
            'success' => false,
            'message' => 'Invalid action or request ID'
        ], 400);
      $result = $this->handleVote($type, $action, $request_id, $user_id, $notes);
      return $result
         ? Response::success(['message' => 'Vote recorded successfully'])
         : Response::error('Failed to record vote');
    }
    protected function getRequestTable(string $type, string $prefix):string
    {
        return match ($type) {
            'term' => $prefix . BASE . 'approval_term_requests',
            default => $prefix . BASE . 'approval_' . $type . '_requests',
        };
    }
    protected function getVoteTable(string $type, string $prefix):string
    {
        return match ($type) {
            'term' => $prefix . BASE . 'approval_term_votes',
            default => $prefix . BASE . 'approval_' . $type . '_votes',
        };
    }
    /**
     * Artist and Term Approvals
     */
    protected function handleVote(string $type, string $vote, int $request_id, int $user_id, string $notes = ''):bool
    {
        if (!in_array($vote, ['approve', 'reject'])) {
            return false;
        }
        global $wpdb;
        $table = $this->getRequestTable($type, $wpdb->prefix);
        $votes = $this->getVoteTable($type, $wpdb->prefix);
   protected function handleVote(string $type, string $vote, int $request_id, int $user_id, string $notes = ''): bool
   {
      if (!in_array($vote, ['approve', 'reject'])) {
         return false;
      }
      $requestTable = $this->getTableName($type, 'requests');
      $voteTable = $this->getTableName($type, 'votes');
        try {
            $request = $wpdb->get_row($wpdb->prepare(
                "SELECT * FROM $table WHERE id = %d",
                $request_id
            ));
            if (!$request || $request->status !== 'pending') {
                throw new Exception("Invalid approval request");
            }
      $requests = CustomTable::for($requestTable);
      $votes = CustomTable::for($voteTable);
            $already_voted = $wpdb->get_row($wpdb->prepare(
                "SELECT * FROM $votes WHERE request_id = %d AND user_id = %d",
                $request_id,
                $user_id
            ));
      try {
         return $requests->transaction(function($requests) use ($votes, $request_id, $user_id, $vote, $notes, $type) {
            // Get the approval request
            $request = $requests->where(['id' => $request_id])->first();
            if ($already_voted && $already_voted->vote !== $vote) {
                $wpdb->update(
                    $votes,
                    [
                        'vote' => $vote,
                    ],
                    [
                        'id' => $already_voted->id
                    ]
                );
                return true;
            } elseif ($already_voted) {
                throw new Exception("User has already voted on this request");
            }
            if (!$request || $request->status !== 'pending') {
               throw new Exception("Invalid approval request");
            }
            $result = $wpdb->insert(
                $votes,
                [
                    'request_id' => $request_id,
                    'user_id'    => $user_id,
                    'vote'       => $vote,
                    'notes'      => $notes,
                    'created_at' => current_time('mysql')
                ]
            );
            if (!$result) {
                throw new Exception("Failed to record vote");
            }
            // Check if user already voted
            $existingVote = $votes->where([
               'request_id' => $request_id,
               'user_id' => $user_id
            ])->first();
            $user = get_userdata($user_id);
            if ($vote === 'approve') {
                $approvers = json_decode($request->approved_by, true)?:[];
            if ($existingVote) {
               if ($existingVote->vote !== $vote) {
                  // Update vote
                  $votes->where(['id' => $existingVote->id])
                     ->updateResults(['vote' => $vote]);
                  return true;
               }
               throw new Exception("User has already voted on this request");
            }
                $approvers[$user_id] = [
                    'name'  => $user->display_name,
                    'voted' => current_time('mysql')
                ];
                $wpdb->update(
                    $table,
                    [
                        'current_approvals' => $request->current_approvals + 1,
                        'updated_at'        => current_time('mysql'),
                        'approved_by'       => $approvers,
                        'expires_at'       => $this->rebuildExpiryDate()
                    ],
                    [
                        'id' => $request_id
                    ]
                );
                if ($request->current_approvals + 1 >= $request->required_approvals) {
                    switch ($type) {
                        case 'user':
                        case 'artist':
                            $this->completeVerification($request_id);
                            break;
                        case 'term':
                            $this->makeTermLive($request);
                            break;
                    }
                }
            } elseif ($vote === 'reject') {
                $rejecters = json_decode($request->rejected_by, true)?:[];
            // Insert new vote
            $votes->create([
               'request_id' => $request_id,
               'user_id' => $user_id,
               'vote' => $vote,
               'notes' => $notes,
            ]);
                $rejecters[$user_id] = [
                    'name'  => $user->display_name,
                    'voted' => current_time('mysql')
                ];
                $wpdb->update(
                    $table,
                    [
                        'current_rejections'    => $request->current_rejections + 1,
                        'rejected_by'           => $rejecters,
                        'updated_at'            => current_time('mysql'),
                        'expires_at'            => $this->rebuildExpiryDate()
                    ],
                    [
                        'id'    => $request_id
                    ]
                );
                if ($request->current_rejections + 1 >= $request->required_approvals) {
                    switch ($type) {
                        case 'user':
                        case 'artist':
                            $this->denyVerification($request_id);
                            break;
                        case 'term':
                            $this->makeTermUnalive($request);
                            break;
                    }
                }
            }
            // Update request based on vote type
            $user = get_userdata($user_id);
            $wpdb->query('COMMIT');
            if ($vote === 'approve') {
               $this->handleApproval($requests, $request, $request_id, $user, $type);
            } else {
               $this->handleRejection($requests, $request, $request_id, $user, $type);
            }
            return true;
        } catch (Exception $e) {
            $wpdb->query('ROLLBACK');
            return true;
         });
      } catch (Exception $e) {
         $this->logError('handleVote', [
            'error' => $e->getMessage(),
            'user_id' => $user_id,
            'request_id' => $request_id,
            'vote' => $vote
         ]);
         return false;
      }
   }
            JVB()->error()
                ->log(
                    '[ApprovalRoutes]:handleVote',
                    "Error creating '.$type.' approval request: " . $e->getMessage(),
                    [
                        'user_id'   => $user_id,
                        'request_id' => $request_id,
                        'vote'       => $vote
                    ]
                );
            return false;
        }
    }
   /**
    * Handle approval vote logic
    */
   protected function handleApproval(CustomTable $table, object $request, int $request_id, $user, string $type): void
   {
      $approvers = json_decode($request->approved_by, true) ?: [];
      $approvers[$user->ID] = [
         'name' => $user->display_name,
         'voted' => current_time('mysql')
      ];
      $table->where(['id' => $request_id])->updateResults([
         'current_approvals' => $request->current_approvals + 1,
         'approved_by' => json_encode($approvers),
         'expires_at' => $this->rebuildExpiryDate()
      ]);
      // Check if threshold met
      if ($request->current_approvals + 1 >= $request->required_approvals) {
         match ($type) {
            'term' => $this->makeTermLive($request),
            default => $this->completeVerification($request_id, $type),
         };
      }
   }
   /**
    * Handle rejection vote logic
    */
   protected function handleRejection(CustomTable $table, object $request, int $request_id, $user, string $type): void
   {
      $rejecters = json_decode($request->rejected_by, true) ?: [];
      $rejecters[$user->ID] = [
         'name' => $user->display_name,
         'voted' => current_time('mysql')
      ];
      $table->where(['id' => $request_id])->updateResults([
         'current_rejections' => $request->current_rejections + 1,
         'rejected_by' => json_encode($rejecters),
         'expires_at' => $this->rebuildExpiryDate()
      ]);
      // Check if threshold met
      if ($request->current_rejections + 1 >= $request->required_approvals) {
         match ($type) {
            'term' => $this->makeTermUnalive($request),
            default => $this->denyVerification($request_id, $type),
         };
      }
   }
    protected function rebuildExpiryDate()
    {
        return date('Y-m-d H:i:s', strtotime("+{$this->expiryDays} days", time()));
    }
    /**
     * @param string $type user/artist or term
     * @param array $request
     *
     * @return bool|int
     */
    protected function createApprovalRequest(string $type, array $request):bool|int
    {
        global $wpdb;
   protected function createApprovalRequest(string $type, array $request): int
   {
      $tableName = $this->getTableName($type, 'requests');
        $table = $this->getRequestTable($type, $wpdb->prefix);
      $id = CustomTable::for($tableName)->create($request);
        $result = $wpdb->insert(
            $table,
            $request
        );
      if (!$id) {
         throw new Exception('Failed to create approval request');
      }
        if (!$result) {
            throw new Exception($wpdb->last_error);
        }
        return $wpdb->insert_id;
    }
      return $id;
   }
    /*************
     * Artist Approvals
@@ -347,60 +285,45 @@
     *
     * @return int|false Request ID or false on failure
     */
    public function createArtistApprovalRequest(int $user_id):int|false
    {
        global $wpdb;
        $wpdb->query('START TRANSACTION');
   /**
    * Create artist approval request - REFACTORED
    */
   public function createArtistApprovalRequest(int $user_id): int|false
   {
      $userRole = jvbUserRole($user_id);
      $tableName = $this->getTableName($userRole, 'requests');
      $table = CustomTable::for($tableName);
        try {
            //Check for existing first
            $table = $this->getRequestTable(jvbUserRole($user_id), $wpdb->prefix);
      try {
         return $table->transaction(function($table) use ($user_id) {
            // Check for existing request
            $existing = $table->where(['user_id' => $user_id])->first();
            // Verify this is not a duplicate request
            $existing = $wpdb->get_var($wpdb->prepare(
                "SELECT id FROM $table
         WHERE user_id = %d",
                $user_id
            ));
            if ($existing) {
               return $existing->id;
            }
            if ($existing) {
                return $existing;
            }
            $user_data = get_userdata($user_id);
            $user_data = get_userdata($user_id);
            $request = [
                'user_id'            => $user_id,
                'status'             => 'pending',
                'expires_at'         => date('Y-m-d H:i:s', strtotime('+30 days')),
                'created_at'         => current_time('mysql'),
                'updated_at'         => current_time('mysql'),
                'name'               => $user_data->display_name,
                'email'              => $user_data->user_email,
            ];
            $result = $this->createApprovalRequest('user', $request);
            if (!$result) {
                throw new Exception($wpdb->last_error);
            }
            $wpdb->query('COMMIT');
            return $result;
        } catch (Exception $e) {
            $wpdb->query('ROLLBACK');
            JVB()->error()
                ->log(
                    '[ApprovalRoutes]:createArtistApprovalRequest',
                    "Error creating artist approval request: " . $e->getMessage(),
                    [
                        'user_id'   => $user_id,
                    ]
                );
            return false;
        }
    }
            return $table->create([
               'user_id' => $user_id,
               'status' => 'pending',
               'expires_at' => date('Y-m-d H:i:s', strtotime('+30 days')),
               'current_approvals' => 0,
               'current_rejections' => 0,
               'required_approvals' => 3, // From config
               'approved_by' => json_encode([]),
               'rejected_by' => json_encode([]),
            ]);
         });
      } catch (Exception $e) {
         $this->logError('createArtistApprovalRequest', [
            'error' => $e->getMessage(),
            'user_id' => $user_id
         ]);
         return false;
      }
   }
    /**
     * Mark an artist as verified
@@ -475,154 +398,57 @@
        return $this->handleVote(jvbUserRole($user_id), $vote, $request_id, $user_id, $notes);
    }
    /**
     * Mark an artist as verified after receiving required approvals
     *
     * @param int $request_id The approval request ID
     *
     * @return bool Success status
     */
    public function completeVerification(int $request_id):bool
    {
        global $wpdb;
        $approval_table = $wpdb->prefix . $this->userRequests;
   /**
    * Complete verification - REFACTORED
    */
   protected function completeVerification(int $request_id, string $type = 'artist'): void
   {
      $tableName = $this->getTableName($type, 'requests');
      $table = CustomTable::for($tableName);
        // Get the request details
        $request = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM $approval_table WHERE id = %d",
            $request_id
        ));
      $table->where(['id' => $request_id])->updateResults([
         'status' => 'approved',
         'approved_at' => current_time('mysql')
      ]);
        if (!$request || $request->status !== 'pending') {
            return false;
        }
      $request = $table->where(['id' => $request_id])->first();
        // Check if enough approvals have been collected
        if ($request->current_approvals < $request->required_approvals) {
            return false;
        }
      if ($request && $request->user_id) {
         $user = new \WP_User($request->user_id);
         $user->add_cap('skip_moderation', true);
        // Start a transaction
        $wpdb->query('START TRANSACTION');
         JVB()->notification()->addNotification(
            $request->user_id,
            'approval_granted',
            ['message' => 'Your account has been verified!']
         );
      }
        try {
            // Get the user ID from the request
            $user_id = $request->user_id;
      $this->cache->flush();
   }
            $this->verifyArtist($user_id, $request->current_approvals);
   protected function denyVerification(int $request_id, string $type = 'artist'): void
   {
      $tableName = $this->getTableName($type, 'requests');
      $table = CustomTable::for($tableName);
            // Update the request status
            $updated = $wpdb->update(
                $approval_table,
                [
                    'status'     => 'approved',
                    'updated_at' => current_time('mysql')
                ],
                [ 'id' => $request_id ]
            );
      $table->where(['id' => $request_id])->updateResults([
         'status' => 'rejected',
         'rejected_at' => current_time('mysql')
      ]);
            if ($updated === false) {
                throw new Exception("Failed to update approval request status");
            }
      $request = $table->where(['id' => $request_id])->first();
            // Notify the user they've been verified
            JVB()->notification()->addNotification(
                $user_id,
                'artist_approved',
                [
                    'request_id'     => $request_id,
                    'approval_date'  => current_time('mysql')
                ]
            );
      if ($request && $request->user_id) {
         JVB()->notification()->addNotification(
            $request->user_id,
            'approval_denied',
            ['message' => 'Your verification request was not approved.']
         );
      }
            $wpdb->query('COMMIT');
            return true;
        } catch (Exception $e) {
            $wpdb->query('ROLLBACK');
            JVB()->error()
                ->log(
                    '[ApprovalRoutes]:completeVerification',
                    "Error verifying user: " . $e->getMessage(),
                    [
                        'user_id'   => $user_id,
                    ]
                );
            return false;
        }
    }
    public function denyVerification(int $request_id):bool
    {
        global $wpdb;
        $approval_table = $wpdb->prefix . $this->userRequests;
        // Get the request details
        $request = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM $approval_table WHERE id = %d",
            $request_id
        ));
        if (!$request || $request->status !== 'pending') {
            return false;
        }
        // Check if enough approvals have been collected
        if ($request->current_rejections < $request->required_approvals) {
            return false;
        }
        // Start a transaction
        $wpdb->query('START TRANSACTION');
        try {
            // Get the user ID from the request
            $user_id = $request->user_id;
            $this->unverifyArtist($user_id, $request->rejected_by);
            // Update the request status
            $updated = $wpdb->update(
                $approval_table,
                [
                    'status'     => 'rejected',
                    'updated_at' => current_time('mysql')
                ],
                [ 'id' => $request_id ]
            );
            if ($updated === false) {
                throw new Exception("Failed to update approval request status");
            }
            // Notify the user they've been verified
            JVB()->notification()->addNotification(
                $user_id,
                'artist_rejected',
                [
                    'request_id'     => $request_id,
                    'approval_date'  => current_time('mysql')
                ]
            );
            $wpdb->query('COMMIT');
            return true;
        } catch (Exception $e) {
            $wpdb->query('ROLLBACK');
            JVB()->error()
                ->log(
                    '[ApprovalRoutes]:denyVerification',
                    "Error removing artist verification status: " . $e->getMessage(),
                    [
                        'user_id'   => $user_id
                    ]
                );
            return false;
        }
    }
      $this->cache->flush();
   }
    /**
     * Get verification details for a request
@@ -632,40 +458,35 @@
     *
     * @return array|false Verification details or false if not verified
     */
    public function getVerificationDetails(int $requestID, string $type):array|false
    {
        global $wpdb;
   public function getVerificationDetails(int $requestID, string $type): array|false
   {
      $requestTable = CustomTable::for($this->getTableName($type, 'requests'));
      $voteTable = CustomTable::for($this->getTableName($type, 'votes'));
        $approval_table = $this->getRequestTable($type, $wpdb->prefix);
        $votes_table = $this->getVoteTable($type, $wpdb->prefix);
      $request = $requestTable->where(['id' => $requestID])->first(ARRAY_A);
        // Get the approval request
        $request = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM $approval_table
         WHERE id = %d
         ORDER BY updated_at DESC",
            $requestID
        ), ARRAY_A);
      if (!$request) {
         return false;
      }
        if (!$request) {
            return false;
        }
      // Get the votes for this request
      $votes = $voteTable
         ->where(['request_id' => $request['id']])
         ->orderBy('created_at', 'ASC')
         ->getResults(ARRAY_A);
        // Get the votes for this request
        $votes = $wpdb->get_results($wpdb->prepare(
            "SELECT v.*, u.display_name as approver_name
         FROM $votes_table v
         WHERE v.request_id = %d
         ORDER BY v.created_at",
            $request['id']
        ), ARRAY_A);
      // Join with user data for display names
      foreach ($votes as &$vote) {
         $user = get_userdata($vote['user_id']);
         $vote['approver_name'] = $user ? $user->display_name : 'Unknown';
      }
        return [
            'request'           => $request,
            'votes'             => $votes,
            'verification_date' => $request['updated_at'],
        ];
    }
      return [
         'request' => $request,
         'votes' => $votes,
         'verification_date' => $request['updated_at'],
      ];
   }
    /*************
     * Term Approvals
@@ -682,80 +503,69 @@
     *
     * @return boolean Success or failure
     */
    protected function makeTermLive(object $request):bool
    {
        global $wpdb;
   protected function makeTermLive(object $request): bool
   {
      try {
         $taxonomy = $request->taxonomy;
         $term_name = $request->name;
         $parent = $request->parent;
        try {
            // Get term data from request
            $taxonomy         = $request->taxonomy;
            $term_name        = $request->name;
            $parent           = $request->parent;
         $result = wp_insert_term($term_name, $taxonomy, [
            'parent' => $parent
         ]);
            $result = wp_insert_term($term_name, $taxonomy, [
                'parent'    => $parent
            ]);
         if (is_wp_error($result)) {
            throw new Exception($result->get_error_message());
         }
            if (is_wp_error($result)) {
                throw new Exception($result->get_error_message());
            }
            $term_id = $result['term_id'];
         $term_id = $result['term_id'];
            $table = $this->getRequestTable('term', $wpdb);
            // Update request status
            $wpdb->update(
                $table,
                [
                    'status'     => 'approved',
                    'updated_at' => current_time('mysql'),
                    'created_term' => $term_id
                ],
                [ 'id' => $request->id ]
            );
         // Update request status
         CustomTable::for($this->getTableName('term', 'requests'))
            ->where(['id' => $request->id])
            ->updateResults([
               'status' => 'approved',
               'created_term' => $term_id
            ]);
            $userIDs = [];
            $approvedBy = [];
            $approvors = json_decode($request->approved_by, true) ?: [];
            $requesters = json_decode($request->requested_by, true) ?: [];
            $rejectors = json_decode($request->rejected_by, true) ?: [];
            foreach (array_merge($requesters, $approvors, $rejectors) as $user_id => $info) {
                $userIDs[] = $user_id;
            }
            foreach ($approvors as $user_id => $info) {
                $approvedBy[] = $info['name'];
            }
         $userIDs = [];
         $approvedBy = [];
         $approvers = json_decode($request->approved_by, true) ?: [];
         $requesters = json_decode($request->requested_by, true) ?: [];
         $rejectors = json_decode($request->rejected_by, true) ?: [];
            $approvedBy = jvbCommaList($approvedBy);
         foreach (array_merge($requesters, $approvers, $rejectors) as $user_id => $info) {
            $userIDs[] = $user_id;
         }
         foreach ($approvers as $user_id => $info) {
            $approvedBy[] = $info['name'];
         }
            // Notify the requester
            JVB()->notification()->addNotification(
                $userIDs,
                'term_approved',
                [
                    'term_id'     => $term_id,
                    'term_name'   => $term_name,
                    'taxonomy'    => $taxonomy,
                    'approved_by' => $approvedBy
                ]
            );
         $approvedBy = jvbCommaList($approvedBy);
            return true;
        } catch (Exception $e) {
            JVB()->error()
                ->log(
                    '[ApprovalRoutes]:makeTermLive',
                    "Error making term live: " . $e->getMessage(),
                    [
                        'request_id'    => $request->id,
                        'requester'     => $request->requested_by,
                        'term_name'     => $term_name,
                        'taxonomy'      => $taxonomy
                    ]
                );
         JVB()->notification()->addNotification(
            $userIDs,
            'term_approved',
            [
               'term_id' => $term_id,
               'term_name' => $term_name,
               'taxonomy' => $taxonomy,
               'approved_by' => $approvedBy
            ]
         );
            return false;
        }
    }
         return true;
      } catch (Exception $e) {
         $this->logError('makeTermLive', [
            'error' => $e->getMessage(),
            'request_id' => $request->id,
            'term_name' => $term_name ?? '',
            'taxonomy' => $taxonomy ?? ''
         ]);
         return false;
      }
   }
    /**
     * Reject a proposed term
     *
@@ -763,64 +573,54 @@
     *
     * @return boolean Success or failure
     */
    protected function makeTermUnalive(object $request):bool
    {
        global $wpdb;
   protected function makeTermUnalive(object $request): bool
   {
      try {
         // Update request status
         CustomTable::for($this->getTableName('term', 'requests'))
            ->where(['id' => $request->id])
            ->updateResults([
               'status' => 'rejected'
            ]);
        try {
            // Update request status
            $wpdb->update(
                $this->getRequestTable('term', $wpdb),
                [
                    'status'     => 'rejected',
                    'updated_at' => current_time('mysql'),
                ],
                [ 'id' => $request->id ]
            );
         $userIDs = [];
         $rejectedBy = [];
            $userIDs = [];
            $rejectedBy = [];
         $approvers = json_decode($request->approved_by, true) ?: [];
         $requesters = json_decode($request->requested_by, true) ?: [];
         $rejectors = json_decode($request->rejected_by, true) ?: [];
            $approvors = json_decode($request->approved_by, true) ?: [];
            $requesters = json_decode($request->requested_by, true) ?: [];
            $rejectors = json_decode($request->rejected_by, true) ?: [];
            foreach (array_merge($requesters, $approvors, $rejectors) as $user_id => $info) {
                $userIDs[] = $user_id;
            }
            foreach ($rejectors as $user_id => $info) {
                $rejectedBy[] = $info['name'];
            }
         foreach (array_merge($requesters, $approvers, $rejectors) as $user_id => $info) {
            $userIDs[] = $user_id;
         }
         foreach ($rejectors as $user_id => $info) {
            $rejectedBy[] = $info['name'];
         }
            $rejectedBy = jvbCommaList($rejectedBy);
         $rejectedBy = jvbCommaList($rejectedBy);
            // Notify the requester
            JVB()->notification()->addNotification(
                $userIDs,
                'term_rejected',
                [
                    'term_name'   => $request->name,
                    'taxonomy'    => $request->taxonomy,
                    'rejected_by' => $rejectedBy
                ]
            );
         JVB()->notification()->addNotification(
            $userIDs,
            'term_rejected',
            [
               'term_name' => $request->name,
               'taxonomy' => $request->taxonomy,
               'rejected_by' => $rejectedBy
            ]
         );
            return true;
        } catch (Exception $e) {
            JVB()->error()
                ->log(
                    '[ApprovalRoutes]:makeTermUnalive',
                    "Error rejecting term: " . $e->getMessage(),
                    [
                        'request_id'    => $request->id,
                        'requester'     => $request->requested_by,
                        'term_name'     => $request->name,
                        'taxonomy'      => $request->taxonomy
                    ]
                );
         return true;
      } catch (Exception $e) {
         $this->logError('makeTermUnalive', [
            'error' => $e->getMessage(),
            'request_id' => $request->id,
            'term_name' => $request->name ?? '',
            'taxonomy' => $request->taxonomy ?? ''
         ]);
            return false;
        }
    }
         return false;
      }
   }
    /**
     * Create a new term approval request
@@ -833,179 +633,150 @@
     *
     * @return int|false Request ID or false on failure
     */
    public function createTermApprovalRequest(
        int $user_id,
        string $taxonomy,
        string $name,
        int $parent = 0,
        int $required_approvals = 3
    ):int|false {
        global $wpdb;
        $table = $this->getRequestTable('term', $wpdb);
   public function createTermApprovalRequest(
      int $user_id,
      string $taxonomy,
      string $name,
      int $parent = 0,
      int $required_approvals = 3
   ): int|false {
      $table = CustomTable::for($this->getTableName('term', 'requests'));
        try {
            $wpdb->query('START TRANSACTION');
            // Step 1: Check if user already has a pending request for this term
            $existing = $wpdb->get_row($wpdb->prepare(
                "SELECT id, requested_by FROM $table
            WHERE name = %s
            AND taxonomy = %s
            AND parent = %d
            AND status = 'pending'",
                $name,
                $taxonomy,
                $parent
            ));
      try {
         return $table->transaction(function($table) use ($user_id, $taxonomy, $name, $parent, $required_approvals) {
            // Check for existing request
            $existing = $table->where([
               'name' => $name,
               'taxonomy' => $taxonomy,
               'parent' => $parent,
               'status' => 'pending'
            ])->first();
            if ($existing) {
                // Decode the requested_by JSON field
                $requestedBy = json_decode($existing->requested_by, true) ?: [];
            if ($existing) {
               $requestedBy = json_decode($existing->requested_by, true) ?: [];
                // Check if this user has already requested this term
                if (isset($requestedBy[$user_id])) {
                    $wpdb->query('COMMIT');
                    return (int)$existing->id;
                }
               if (isset($requestedBy[$user_id])) {
                  return (int)$existing->id;
               }
                // Add this user to the requesters
                $requestedBy[$user_id] = get_userdata($user_id)->display_name;
               $requestedBy[$user_id] = get_userdata($user_id)->display_name;
                // Update the request with the new requester
                $updated = $wpdb->update(
                    $table,
                    ['requested_by' => json_encode($requestedBy)],
                    ['id' => $existing->id]
                );
               $table->where(['id' => $existing->id])->updateResults([
                  'requested_by' => json_encode($requestedBy)
               ]);
                if (!$updated) {
                    throw new Exception($wpdb->last_error);
                }
               return (int)$existing->id;
            }
                $wpdb->query('COMMIT');
                return (int)$existing->id;
            }
            // Create new request
            return $this->createApprovalRequest('term', [
               'taxonomy' => $taxonomy,
               'name' => $name,
               'parent' => $parent ?: null,
               'status' => 'pending',
               'required_approvals' => $required_approvals,
               'current_approvals' => 0,
               'current_rejections' => 0,
               'requested_by' => json_encode([$user_id => get_userdata($user_id)->display_name]),
               'expires_at' => date('Y-m-d H:i:s', strtotime('+30 days')),
            ]);
         });
      } catch (Exception $e) {
         $this->logError('createTermApprovalRequest', [
            'error' => $e->getMessage(),
            'user_id' => $user_id,
            'taxonomy' => $taxonomy,
            'name' => $name
         ]);
            $request = [
                'taxonomy'  => $taxonomy,
                'name'      => $name,
                'parent'    => $parent ?: null,
                'status' => 'pending',
                'required_approvals' => $required_approvals,
                'current_approvals' => 0,
                'current_rejections' => 0,
                'requested_by' => json_encode([$user_id => get_userdata($user_id)->display_name]),
                'expires_at' => date('Y-m-d H:i:s', strtotime('+30 days')),
                'created_at' => current_time('mysql'),
                'updated_at' => current_time('mysql')
            ];
            $result = $this->createApprovalRequest('term', $request);
            if (!$result) {
                throw new Exception($wpdb->last_error);
            }
            $request_id = $wpdb->insert_id;
            $wpdb->query('COMMIT');
            return $request_id;
        } catch (Exception $e) {
            $wpdb->query('ROLLBACK');
            JVB()->error()
                ->log(
                    '[ApprovalRoutes]:createTermApprovalRequest',
                    "Error creating term approval request: " . $e->getMessage(),
                    [
                        'user_id'   => $user_id,
                        'taxonomy' => $taxonomy,
                        'name'       => $name
                    ]
                );
            return false;
        }
    }
         return false;
      }
   }
    /**
     * Clean up expired approval requests and notify admin
     *
     * @return void
     */
    public function cleanupExpiredApprovals(): void
    {
        global $wpdb;
        $tables = array_map(function ($table) use ($wpdb){
            return $wpdb->prefix . BASE . 'approval_'.$table.'_requests';
        }, $this->allTypes);
   public function cleanupExpiredApprovals(): void
   {
      $now = current_time('mysql');
      foreach ($this->allTypes as $type) {
         $tableName = $this->getTableName($type, 'requests');
        foreach ($tables as $table) {
            $wpdb->query($wpdb->prepare(
                "UPDATE $table SET status = 'expired', updated_at = %s
        WHERE status = 'pending' AND expires_at < %s",
                current_time('mysql'),
                current_time('mysql')
            ));
        }
         CustomTable::for($tableName)->query(
            "UPDATE {table}
                 SET status = 'expired'
                 WHERE status = 'pending'
                   AND expires_at < %s",
            [$now]
         );
      }
        // Clear caches
        CacheManager::invalidateGroup('approvals');
    }
      $this->cache->flush();
   }
    public function getApprovals(WP_REST_Request $request)
    {
        $user_id = get_current_user_id();
        $params = $request->get_params();
        $type = $params['type'] ?? 'all';
        $status = $params['status'] ?? 'pending';
   protected function getTableName(string $type, string $suffix): string
   {
      return match ($type) {
         'term' => "approval_term_{$suffix}",
         default => "approval_{$type}_{$suffix}",
      };
   }
        // Get appropriate approvals based on type
        if ($type === 'user' || $type === 'all') {
            $user_approvals = $this->getUserApprovals($status);
        } else {
            $user_approvals = [];
        }
   public function getApprovals(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      $type = sanitize_text_field($request->get_param('type') ?? 'all');
      $status = sanitize_text_field($request->get_param('status') ?? 'pending');
        if ($type === 'term' || $type === 'all') {
            $term_approvals = $this->getTermApprovals($status);
        } else {
            $term_approvals = [];
        }
      if (!$this->checkUser($user_id)) {
         return $this->unauthorized();
      }
        return new WP_REST_Response([
            'user_approvals' => $user_approvals,
            'term_approvals' => $term_approvals
        ]);
    }
      $cacheKey = compact('user_id', 'type', 'status');
    private function getUserApprovals(string $status = 'pending'): array
    {
        global $wpdb;
        $table = $wpdb->prefix . $this->userRequests;
      $result = $this->cache->remember($cacheKey, function() use ($type, $status) {
         $data = [];
        // Build the status condition
        $status_condition = ($status === 'all') ?
            "status IN ('pending', 'approved', 'rejected', 'expired')" :
            $wpdb->prepare("status = %s", $status);
         if ($type === 'user' || $type === 'all') {
            $data['user_approvals'] = $this->getUserApprovals($status);
         }
        return $wpdb->get_results(
            "SELECT * FROM $table
        WHERE $status_condition
        ORDER BY created_at DESC"
        );
    }
         if ($type === 'term' || $type === 'all') {
            $data['term_approvals'] = $this->getTermApprovals($status);
         }
    private function getTermApprovals(string $status = 'pending'): array
    {
        global $wpdb;
        $table = $wpdb->prefix . $this->termRequests;
         return $data;
      });
        // Build the status condition
        $status_condition = ($status === 'all') ?
            "status IN ('pending', 'approved', 'rejected', 'expired')" :
            $wpdb->prepare("status = %s", $status);
      return $this->success($result);
   }
        return $wpdb->get_results(
            "SELECT * FROM $table
        WHERE $status_condition
        ORDER BY created_at DESC"
        );
    }
   private function getUserApprovals(string $status = 'pending'): array
   {
      $table = CustomTable::for($this->getTableName('artist', 'requests'));
      $query = $table;
      if ($status !== 'all') {
         $query = $query->where(['status' => $status]);
      }
      return $query->orderBy('created_at', 'DESC')->getResults(ARRAY_A);
   }
   private function getTermApprovals(string $status = 'pending'): array
   {
      $table = CustomTable::for($this->getTableName('term', 'requests'));
      if ($status === 'all') {
         return $table->orderBy('created_at', 'DESC')->getResults(ARRAY_A);
      }
      return $table
         ->where(['status' => $status])
         ->orderBy('created_at', 'DESC')
         ->getResults(ARRAY_A);
   }
}