Jake Vanderwerf
2026-05-01 48721c85ebcfa973ee81719d2467ca80e4253dc9
inc/rest/routes/ApprovalRoutes.php
@@ -33,18 +33,13 @@
        parent::__construct();
        $this->initTypes();
        if ($this->hasMemberApproval) {
            add_action('user_register', [$this, 'handleNewUserRegistration'], 10, 2);
        }
        add_action('jvb_cleanup_expired_approvals', [$this, 'cleanupExpiredApprovals']);
    }
    protected function initTypes():void
    {
        $this->userTypes = [];
        $this->termTypes = [];
      $this->allTypes = [];
        if ($this->hasMemberApproval) {
            $this->userTypes = Registrar::getFeatured('approve_new', 'user');
            $this->allTypes = $this->userTypes;
@@ -79,27 +74,6 @@
         ->register();
    }
    /**
     * Handler for user registration
     *
     * @param int $user_id New user ID
     * @param object $user the new user object
     *
     * @return void
     */
   public function handleNewUserRegistration(int $user_id, object $user): void
   {
      $intersect = array_intersect(
         array_map(fn($role) => BASE.$role, $this->userTypes),
         (array) $user->roles
      );
      if (!empty($intersect)) {
         $user->add_cap('skip_moderation', false);
         $this->createArtistApprovalRequest($user_id);
      }
   }
    /**
     * @param WP_REST_Request $request
@@ -131,255 +105,25 @@
     */
   protected function handleVote(string $type, string $vote, int $request_id, int $user_id, string $notes = ''): bool
   {
      if (!in_array($vote, ['approve', 'reject'])) {
      if (!in_array($vote, ['approve', 'reject', 'dismiss'])) {
         return false;
      }
      $requestTable = $this->getTableName($type, 'requests');
      $voteTable = $this->getTableName($type, 'votes');
      $requests = CustomTable::for($requestTable);
      $votes = CustomTable::for($voteTable);
      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 (!$request || $request->status !== 'pending') {
               throw new Exception("Invalid approval request");
            }
            // Check if user already voted
            $existingVote = $votes->where([
               'request_id' => $request_id,
               'user_id' => $user_id
            ])->first();
            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");
            }
            // Insert new vote
            $votes->create([
               'request_id' => $request_id,
               'user_id' => $user_id,
               'vote' => $vote,
               'notes' => $notes,
            ]);
            // Update request based on vote type
            $user = get_userdata($user_id);
            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) {
         $this->logError('handleVote', [
            'error' => $e->getMessage(),
            'user_id' => $user_id,
            'request_id' => $request_id,
            'vote' => $vote
         ]);
         return false;
      }
      $result = JVB()->approvals()->markApproval($request_id, $user_id, $type, $vote, $notes);
      return $result['success'];
   }
   /**
    * 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): int
   {
      $tableName = $this->getTableName($type, 'requests');
      $id = CustomTable::for($tableName)->create($request);
      if (!$id) {
         throw new Exception('Failed to create approval request');
      }
      return $id;
   }
    /*************
     * Artist Approvals
     ************/
    /**
     * Create artist approval request
     *
     * @param int $user_id User ID to be approved
     *
     * @return int|false Request ID or false on failure
     */
   /**
    * 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 {
         return $table->transaction(function($table) use ($user_id) {
            // Check for existing request
            $existing = $table->where(['user_id' => $user_id])->first();
            if ($existing) {
               return $existing->id;
            }
            $user_data = get_userdata($user_id);
            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
     *
     * @param int $user_id The user to verify
     * @param int $verified_by ID of user who verified them (optional)
     *
     * @return bool Success status
     */
    public function verifyArtist(int $user_id, int $verified_by = 0):bool
    {
        $user = get_userdata($user_id);
        // Check if user has the artist role
        if (!array_intersect(array_map(function ($role) { return BASE.$role; }, $this->userTypes), $user->roles)) {
            return false;
        }
        // Add the capability
        $user->add_cap('skip_moderation', true);
        // Store verification metadata
        update_user_meta($user_id, BASE . 'verification_date', current_time('mysql'));
        if ($verified_by) {
            update_user_meta($user_id, BASE . 'verified_by', $verified_by);
        }
        return true;
    }
    /**
     * Mark an artist as verified
     *
     * @param int $user_id The user to verify
     * @param int $verified_by ID of user who verified them (optional)
     *
     * @return bool Success status
     */
    public function unverifyArtist(int $user_id, int $verified_by = 0):bool
    {
        $user = get_userdata($user_id);
        // Check if user has the artist role
        if (!array_intersect(array_map(function ($role) { return BASE.$role; }, $this->userTypes), $user->roles)) {
            return false;
        }
        // Add the capability
        $user->add_cap('skip_moderation', false);
        // Store verification metadata
        update_user_meta($user_id, BASE . 'unverification_date', current_time('mysql'));
        if ($verified_by) {
            update_user_meta($user_id, BASE . 'unverified_by', $verified_by);
        }
        return true;
    }
    /**
     * Record an approval vote for an artist
     *
     * @param int $user_id User casting the approval vote
@@ -394,57 +138,6 @@
        return $this->handleVote(jvbUserRole($user_id), $vote, $request_id, $user_id, $notes);
    }
   /**
    * Complete verification - REFACTORED
    */
   protected function completeVerification(int $request_id, string $type = 'artist'): void
   {
      $tableName = $this->getTableName($type, 'requests');
      $table = CustomTable::for($tableName);
      $table->where(['id' => $request_id])->updateResults([
         'status' => 'approved',
         'approved_at' => current_time('mysql')
      ]);
      $request = $table->where(['id' => $request_id])->first();
      if ($request && $request->user_id) {
         $user = new \WP_User($request->user_id);
         $user->add_cap('skip_moderation', true);
         JVB()->notification()->addNotification(
            $request->user_id,
            'approval_granted',
            ['message' => 'Your account has been verified!']
         );
      }
      $this->cache->flush();
   }
   protected function denyVerification(int $request_id, string $type = 'artist'): void
   {
      $tableName = $this->getTableName($type, 'requests');
      $table = CustomTable::for($tableName);
      $table->where(['id' => $request_id])->updateResults([
         'status' => 'rejected',
         'rejected_at' => current_time('mysql')
      ]);
      $request = $table->where(['id' => $request_id])->first();
      if ($request && $request->user_id) {
         JVB()->notification()->addNotification(
            $request->user_id,
            'approval_denied',
            ['message' => 'Your verification request was not approved.']
         );
      }
      $this->cache->flush();
   }
    /**
     * Get verification details for a request
@@ -456,25 +149,16 @@
     */
   public function getVerificationDetails(int $requestID, string $type): array|false
   {
      $requestTable = CustomTable::for($this->getTableName($type, 'requests'));
      $voteTable = CustomTable::for($this->getTableName($type, 'votes'));
      $request = $requestTable->where(['id' => $requestID])->first(ARRAY_A);
      $request = JVB()->approvals()->getRequest($requestID, $type);
      if (!$request) {
         return false;
      }
      // Get the votes for this request
      $votes = $voteTable
         ->where(['request_id' => $request['id']])
         ->orderBy('created_at', 'ASC')
         ->getResults(ARRAY_A);
      $votes = JVB()->approvals()->getVotes($requestID, $type);
      // 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';
         $vote['approver_name'] = $user ? jvbGetUsername($vote['user_id']) : 'Someone';
      }
      return [
@@ -484,139 +168,7 @@
      ];
   }
    /*************
     * Term Approvals
     ************/
    public function voteForTerm(int $user_id, int $request_id, string $vote, string $notes = ''):bool
    {
        return $this->handleVote('term', $vote, $user_id, $request_id, $notes);
    }
    /**
     * Publish an approved term
     *
     * @param object $request Approval request object
     *
     * @return boolean Success or failure
     */
   protected function makeTermLive(object $request): bool
   {
      try {
         $taxonomy = $request->taxonomy;
         $term_name = $request->name;
         $parent = $request->parent;
         $result = wp_insert_term($term_name, $taxonomy, [
            'parent' => $parent
         ]);
         if (is_wp_error($result)) {
            throw new Exception($result->get_error_message());
         }
         $term_id = $result['term_id'];
         // Update request status
         CustomTable::for($this->getTableName('term', 'requests'))
            ->where(['id' => $request->id])
            ->updateResults([
               'status' => 'approved',
               'created_term' => $term_id
            ]);
         $userIDs = [];
         $approvedBy = [];
         $approvers = json_decode($request->approved_by, true) ?: [];
         $requesters = json_decode($request->requested_by, true) ?: [];
         $rejectors = json_decode($request->rejected_by, true) ?: [];
         foreach (array_merge($requesters, $approvers, $rejectors) as $user_id => $info) {
            $userIDs[] = $user_id;
         }
         foreach ($approvers as $user_id => $info) {
            $approvedBy[] = $info['name'];
         }
         $approvedBy = jvbCommaList($approvedBy);
         JVB()->notification()->addNotification(
            $userIDs,
            'term_approved',
            [
               'term_id' => $term_id,
               'term_name' => $term_name,
               'taxonomy' => $taxonomy,
               'approved_by' => $approvedBy
            ]
         );
         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
     *
     * @param object $request request object
     *
     * @return boolean Success or failure
     */
   protected function makeTermUnalive(object $request): bool
   {
      try {
         // Update request status
         CustomTable::for($this->getTableName('term', 'requests'))
            ->where(['id' => $request->id])
            ->updateResults([
               'status' => 'rejected'
            ]);
         $userIDs = [];
         $rejectedBy = [];
         $approvers = json_decode($request->approved_by, true) ?: [];
         $requesters = json_decode($request->requested_by, true) ?: [];
         $rejectors = json_decode($request->rejected_by, true) ?: [];
         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);
         JVB()->notification()->addNotification(
            $userIDs,
            'term_rejected',
            [
               'term_name' => $request->name,
               'taxonomy' => $request->taxonomy,
               'rejected_by' => $rejectedBy
            ]
         );
         return true;
      } catch (Exception $e) {
         $this->logError('makeTermUnalive', [
            'error' => $e->getMessage(),
            'request_id' => $request->id,
            'term_name' => $request->name ?? '',
            'taxonomy' => $request->taxonomy ?? ''
         ]);
         return false;
      }
   }
    /**
     * Create a new term approval request
@@ -636,80 +188,14 @@
      int $parent = 0,
      int $required_approvals = 3
   ): int|false {
      $table = CustomTable::for($this->getTableName('term', 'requests'));
      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) {
               $requestedBy = json_decode($existing->requested_by, true) ?: [];
               if (isset($requestedBy[$user_id])) {
                  return (int)$existing->id;
               }
               $requestedBy[$user_id] = get_userdata($user_id)->display_name;
               $table->where(['id' => $existing->id])->updateResults([
                  'requested_by' => json_encode($requestedBy)
               ]);
               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
         ]);
         return false;
      }
   }
    /**
     * Clean up expired approval requests and notify admin
     *
     * @return void
     */
   public function cleanupExpiredApprovals(): void
   {
      $now = current_time('mysql');
      foreach ($this->allTypes as $type) {
         $tableName = $this->getTableName($type, 'requests');
         CustomTable::for($tableName)->query(
            "UPDATE {table}
                 SET status = 'expired'
                 WHERE status = 'pending'
                   AND expires_at < %s",
            [$now]
         );
      }
      $this->cache->flush();
      $result = JVB()->approvals()->createApproval(
         $user_id,
         $taxonomy,
         $name,
         $parent
      );
      return $result['success'];
   }
   protected function getTableName(string $type, string $suffix): string
@@ -751,6 +237,7 @@
   private function getUserApprovals(string $status = 'pending'): array
   {
      $table = CustomTable::for($this->getTableName('artist', 'requests'));
      $query = $table;