Jake Vanderwerf
2026-05-01 48721c85ebcfa973ee81719d2467ca80e4253dc9
inc/managers/ApprovalManager.php
@@ -3,8 +3,8 @@
use DateTime;
use JVBase\base\Site;
use JVBase\registrar\Registrar;
use JVBase\managers\CustomTable;
if (!defined('ABSPATH')) {
   exit;
@@ -14,12 +14,20 @@
   protected array $tables =[];
   protected int $expiresAt = 21; //days for request
   protected int $requiredVotes = 3; //Number of votes before a term is approved
   protected bool $hasApproval = false;
   public function __construct()
   {
      $this->defineTables();
      if (empty($this->tables)) {
         return;
      }
      $this->hasApproval = Site::membership() && Site::membership()->has('member_verified');
      if ($this->hasApproval) {
         add_action('user_register', [$this, 'handleRegistration'], 10, 2);
      }
      add_action('jvb_cleanup_expired_approvals', [$this, 'cleanupExpired']);
   }
   protected function defineTables():void
@@ -27,6 +35,7 @@
      $types = Registrar::getFeatured('approve_new');
      foreach ($types as $type) {
         $requests = CustomTable::for("approval_{$type}_requests");
         $registrar = Registrar::getInstance($type);
         $requests->setColumns([
            'id'           => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
@@ -54,9 +63,9 @@
         ]);
         $base = BASE;
         $requests->setConstraints([
            "CONSTRAINTS `{$base}{$type}_approval_requester` FOREIGN KEY (`user_id`)
            "CONSTRAINT `{$base}{$type}_approval_requester` FOREIGN KEY (`user_id`)
            REFERENCES `{$requests->getUserTable()}` (`ID`) ON DELETE CASCADE",
            "CONSTRAINTS `{$base}{$type}_approval_parent_term` FOREIGN KEY (`parent_id`)
            "CONSTRAINT `{$base}{$type}_approval_parent_term` FOREIGN KEY (`parent_id`)
            REFERENCES `{$requests->getTermTable()}` (`term_id`) ON DELETE CASCADE"
         ]);
         $requests->defineTable();
@@ -81,7 +90,7 @@
         $votes->setConstraints([
            "CONSTRAINT `{$base}{$type}_user_approval_request` FOREIGN KEY (`request_id`)
            REFERENCES `{$requests->getFullTableName()} (`id`) ON DELETE CASCADE",
            REFERENCES `{$requests->getFullTableName()}` (`id`) ON DELETE CASCADE",
            "CONSTRAINT `{$base}{$type}_user_approval_voter` FOREIGN KEY (`user_id`)
            REFERENCES `{$votes->getUserTable()}` (`ID`) ON DELETE CASCADE"
         ]);
@@ -94,7 +103,23 @@
         ];
      }
   }
   /**
    * Handler for user registration
    *
    * @param int $user_id New user ID
    * @param object $user the new user object
    *
    * @return void
    */
   public function handleRegistration(int $user_id, object $user): void
   {
      $registrar = Registrar::getInstance(jvbUserRole($user_id));
      if ($registrar && $registrar->hasFeature('approve_new')) {
         $user->add_cap('skip_moderation', false);
         $this->createApproval($user_id, $registrar->getSlug(), $user->display_name);
      }
   }
   public function canApprove(int $userID, string $type):bool
   {
      $type = jvbNoBase($type);
@@ -198,6 +223,8 @@
         return $this->response(false, 'Invalid vote');
      }
      $response = $votes->findOrCreate([
         'request_id'   => $request_id,
         'user_id'      => $userID
@@ -209,12 +236,37 @@
      if (!$response) {
         return $this->response(false, 'Could not store vote for some reason.');
      }
      $approvers = $request['approved_by'];
      $rejectors = $request['rejected_By'];
      $both = array_merge($approvers, $rejectors);
      //See if the user has already voted
      //If the user is changing their vote, proceed. If it's the same, we can bail early.
      if (in_array($userID, $both)) {
         switch ($vote) {
            case 'reject':
               if (in_array($userID, $approvers)) {
                  unset($approvers[array_search($userID, $approvers)]);
               } else {
                  return $this->response(true, 'You already voted this way.');
               }
               break;
            case 'approve':
               if (in_array($userID, $rejectors)) {
                  unset($rejectors[array_search($userID, $rejectors)]);
               } else {
                  return $this->response(true, 'You already voted this way.');
               }
         }
      }
      switch ($vote) {
         case 'reject':
            $rejectors[] = $userID;
            $request['rejections']++;
            break;
         case 'approve':
            $approvers[] = $userID;
            $request['approvals']++;
            break;
         default:
@@ -227,8 +279,10 @@
         $this->finalizeApproval($request_id, $type);
      }
      $updated = $requests->update([
         'rejections'=> $request['rejections'],
         'approvals' => $request['approvals']
         'rejections'   => $request['rejections'],
         'approvals'    => $request['approvals'],
         'rejected_by'  => $rejectors,
         'approved_by'  => $approvers
      ],
      [
         'request_id' => $request_id
@@ -298,7 +352,9 @@
            $updatedID = $term['term_id'];
         }
      } elseif ($registrar->getType() === 'user') {
         $user = get_userdata($request['user_id']);
         $user->add_cap('skip_moderation', true);
         update_user_meta($request['user_id'], BASE.'verification_date', current_time('mysql'));
      }
      $updates = [
@@ -314,7 +370,7 @@
         ]);
      JVB()->notification()->addNotification($request['user_id'], $type.'approved', null, 'Your suggestion "'.$request['name'].'" was denied.');
      JVB()->notification()->notify($request['user_id'], $type.'approved', null, 'Your suggestion "'.$request['name'].'" was denied.');
   }
   public function finalizeDenial(int $request_id, string $type):void
   {
@@ -331,7 +387,7 @@
            'request_id' => $request_id
         ]);
      JVB()->notification()->addNotification($request['user_id'], $type.'denied', null, 'Your suggestion "'.$request['name'].'" was denied.');
      JVB()->notification()->notify($request['user_id'], $type.'denied', null, 'Your suggestion "'.$request['name'].'" was denied.');
   }
   public function getApprovalRequests(int $userID, ?string $type = null):array
@@ -429,4 +485,74 @@
         ]
      ];
   }
   /**
    * 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);
      $table = $this->tables[$userRole]['requests']??false;
      if (!$table) {
         return false;
      }
      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([]),
         ]);
      });
   }
   public function getRequest(int $requestID, string $type):array|false
   {
      $table = $this->requests($type);
      if (!$table) {
         return false;
      }
      return $table->get(['id' => $requestID])??false;
   }
   public function getVotes(int $requestID, string $type):array|false
   {
      $table = $this->votes($type);
      if (!$table) {
         return false;
      }
      return $table->getMany(['request_id' => $requestID])??false;
   }
   public function cleanupExpired():void
   {
      $now = current_time('mysql');
      foreach ($this->tables as $type => $tables) {
         $tables['requests']->query(
            "UPDATE {table}
            SET status = 'expired'
            WHERE status = 'pending'
            AND expires_at <%s",
            [$now]
         );
      }
   }
}