Jake Vanderwerf
2026-09-06 3e4e3db64b366028845cfd610c5b17f26bd062ee
inc/managers/InvitationsManager.php
@@ -2,9 +2,14 @@
namespace JVBase\managers;
use Exception;
use JVBase\Instance;
use JVBase\managers\queue\executors\InvitationExecutor;
use JVBase\managers\queue\Queue;
use JVBase\managers\queue\TypeConfig;
use JVBase\utility\Features;
use JVBase\registrar\Registrar;
use JVBase\base\Site;
use JVBase\rest\Rest;
use JVBase\rest\routes\Invitations;
use WP_Error;
if (!defined('ABSPATH')) {
@@ -13,13 +18,24 @@
class InvitationsManager
{
   use Instance;
   protected array $inviteConfig;
   protected CustomTable $table;
   protected int $expiryDays = 14;
   public function __construct()
   protected Cache $cache;
   protected Rest $route;
   private function __construct()
   {
      $this->defineTable();
      if (!isset($this->table)) {
         return;
      }
      if (Site::membership() && Site::membership()->has('can_invite')) {
         $this->route = Invitations::getInstance();
      }
      $this->setInviteConfig();
      $this->table = CustomTable::for('invitations');
      $this->cache = Cache::for('invitations')->user();
      add_action('init', [$this, 'registerInvitationExecutors'], 5);
      add_action('user_register', [$this, 'checkInvitation']);
@@ -28,6 +44,61 @@
      add_filter(BASE . 'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
   }
   public function defineTable():void
   {
      $terms = Registrar::withFeature('invitable', 'term');
      $membership = Site::membership();
      $roles = ($membership) ? Site::membership()->has('can_invite') :[];
      if (empty($terms) && empty($roles)) {
         return;
      }
      $table = CustomTable::for('invitations');
      $columns = [
         'id'           => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT',
         'name'            => 'varchar(255) NOT NULL',
         'email'           => 'varchar(255) NOT NULL',
         'invitation_token'   => 'varchar(255) NOT NULL',
         'invited_role'    => 'varchar(50) NOT NULL',    //Role being invited to
         'status'       => "ENUM('pending','accepted','rejected','expired','revoked') DEFAULT 'pending'",
         'inviters'        => 'JSON NOT NULL',        // Array of {user_id, invited_at}
         'new_user_id'     => $table->getUserIDType().' DEFAULT NULL',
         'expires_at'      => 'datetime NOT NULL',
         'accepted_at'     => 'datetime DEFAULT NULL',
         'created_at'      => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP',
         'updated_at'      => 'datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
      ];
      foreach ($terms as $tax) {
         $columns["to_{$tax}"] = $table->getTermIDType().' DEFAULT NULL';
      }
      $table->setColumns($columns);
      $table->setKeys([
         ['key' => 'PRIMARY', 'value' => '(`id`)'],
         ['key' => 'UNIQUE', 'value' => '`unique_email_role` (`email`, `invited_role`)'],
         '`token_lookup` (`invitation_token`)',
         '`status_expiry` (`status`, `expires_at`)',
         '`role_status` (`invited_role`, `status`)',
         '`email_status` (`email`, `status`)'
      ]);
      $constraints = [];
      $base = BASE;
      global $wpdb;
      foreach ($terms as $tax) {
         $constraints[] = "CONSTRAINT `{$base}invitations_{$tax}_fk` FOREIGN KEY (`to_{$tax}`) REFERENCES `{$wpdb->terms}` (`term_id`)
            ON DELETE SET NULL";
      }
      $constraints[] = "CONSTRAINT `{$base}invitations_user_fk`  FOREIGN KEY (`new_user_id`) REFERENCES `{$table->getUserTable()}` (`ID`)
        ON DELETE SET NULL";
      $table->setConstraints($constraints);
      $table->defineTable();
      $this->table = $table;
   }
   protected function setInviteConfig():void
   {
      $this->inviteConfig = get_option(BASE.'invitation_config', [
@@ -57,7 +128,7 @@
    */
   public function registerInvitationExecutors(): void
   {
      $registry = JVB()->queue()->registry();
      $registry = Queue::getInstance()->registry();
      $executor = new InvitationExecutor();
      // Create invitations - chunked at 20
@@ -83,49 +154,62 @@
   /**
    * Build invite types from JVB constants
    * Combines JVB_MEMBERSHIP['can_invite'] and invitable taxonomies
    * TODO: Should be accessible via Site::getInstance()->membership()\
    *
    */
   protected function buildInviteTypes(): array
   {
      $types = [];
      return $this->cache->remember(
         'invitableTypes',
         function () {
            $types = [];
      // Global invitations from JVB_MEMBERSHIP
      if (!empty(JVB_MEMBERSHIP['can_invite'])) {
         foreach (JVB_MEMBERSHIP['can_invite'] as $role => $canInvite) {
            $types[$role] = [
               'can_invite' => $canInvite,
               'to_terms' => []
            ];
         }
      }
            // Global invitations from JVB_MEMBERSHIP
            if (!empty(JVB_MEMBERSHIP['can_invite'])) {
               foreach (JVB_MEMBERSHIP['can_invite'] as $role => $canInvite) {
                  $types[$role] = [
                     'can_invite' => $canInvite,
                     'to_terms' => []
                  ];
               }
            }
      // Term invitations from invitable content taxonomies
      foreach (JVB_TAXONOMY as $taxonomy => $config) {
         if (Features::forTaxonomy($taxonomy)->has('invitable') &&
            Features::forTaxonomy($taxonomy)->has('is_content') &&
            Features::forTaxonomy($taxonomy)->has('is_ownable')) {
            // Term invitations from invitable content taxonomies
            $invitable = Registrar::withFeature('invitable', 'term');
            $content = Registrar::withFeature('is_content', 'term');
            $ownable = Registrar::withFeature('is_ownable', 'term');
            $taxonomies = array_intersect($invitable, $content, $ownable);
            if (!empty($taxonomies)) {
               $users = Registrar::getRegistered('user');
            }
            foreach ($taxonomies as $taxonomy) {
               $registrar = Registrar::getInstance($taxonomy);
            $forContent = $config['for_content'] ?? [];
            foreach ($forContent as $content) {
               // Find which user roles can create this content
               foreach (JVB_USER as $role => $userConfig) {
                  $creatable = Features::forUser($role)->getCreatableContent();
                  if (in_array($content, $creatable)) {
                     if (!isset($types[$role])) {
                        $types[$role] = [
                           'can_invite' => [],
                           'to_terms' => []
                        ];
                     }
                     if (!in_array($taxonomy, $types[$role]['to_terms'])) {
                        $types[$role]['to_terms'][] = $taxonomy;
               foreach ($registrar->registrar->for as $content) {
                  // Find which user roles can create this content
                  foreach ($users as $user) {
                     $userRegistrar = Registrar::getInstance($user);
                     $creatable = $userRegistrar->getCreatable();
                     if (in_array($content, $creatable)) {
                        if (!isset($types[$role])) {
                           $types[$role] = [
                              'can_invite' => [],
                              'to_terms' => []
                           ];
                        }
                        if (!in_array($taxonomy, $types[$role]['to_terms'])) {
                           $types[$role]['to_terms'][] = $taxonomy;
                        }
                     }
                  }
               }
            }
         }
      }
      return $types;
            return $types;
         }
      );
   }
   /******************************************************************
@@ -141,7 +225,7 @@
      }
      // User must be owner or manager of the term
      return JVB()->roles()->isManager($userID, $termID);
      return Roles::getInstance()()->isManager($userID, $termID);
   }
   /**
@@ -308,7 +392,7 @@
      } catch (Exception $e) {
         $this->table->rollback();
         JVB()->error()->log(
         Errors::getInstance()->log(
            'invitation_create',
            'Error processing invitations: ' . $e->getMessage(),
            ['user_id' => $userID],
@@ -467,8 +551,9 @@
         $term = get_term($termID, BASE . $taxonomy);
         if ($term && !is_wp_error($term)) {
            $config = JVB_TAXONOMY[$taxonomy] ?? [];
            $singular = $config['singular'] ?? $taxonomy;
            $registrar = Registrar::getInstance($taxonomy);
            $singular = $registrar ? $registrar->getSingular() : $taxonomy;
            $termContent[] = sprintf(
               "<p>%s has also invited you to join %s. You'll be automatically added to this %s when you register.</p>",
@@ -480,9 +565,9 @@
      }
      $termText = implode('', $termContent);
      $button = JVB()->email()->button($signupUrl, 'Join the Scene!');
      $link = JVB()->email()->link($signupUrl);
      $signature = JVB()->email()->signature();
      $button = Emails::getInstance()->button($signupUrl, 'Join the Scene!');
      $link = Emails::getInstance()->link($signupUrl);
      $signature = Emails::getInstance()->signature();
      $message = sprintf(
         '<p>Hi %s!</p>
@@ -519,7 +604,7 @@
         $signature
      );
      JVB()->email()->sendEmail($email, $subject, $message);
      Emails::getInstance()->sendEmail($email, $subject, $message);
   }
   /**
@@ -581,7 +666,7 @@
         // Grant capabilities
         $user = get_userdata($userID);
         $user->add_cap('skip_moderation', true);
         $user->add_cap('skip_moderation');
         return true;
      });
@@ -597,7 +682,7 @@
      $this->notifyInvitersOfAcceptance($invitation, $userID);
      // Invalidate cache
      Cache::for('invitations')->forget('user_' . $userID);
      $this->cache->forget('user_' . $userID);
      return true;
   }
@@ -607,7 +692,7 @@
    */
   protected function processTermMembership(int $userID, object $invitation): void
   {
      foreach (JVB()->roles()->getInvitableTaxonomies() as $taxonomy) {
      foreach (Roles::getInstance()()->getInvitableTaxonomies() as $taxonomy) {
         $column = 'to_' . $taxonomy;
         if (isset($invitation->$column) && $invitation->$column) {
            $termID = (int) $invitation->$column;
@@ -632,7 +717,7 @@
      $userData = get_userdata($userID);
      foreach ($inviters as $inviter) {
         JVB()->notification()->addNotification(
         NotificationManager::getInstance()->addNotification(
            $inviter['user_id'],
            'invitation_accepted',
            [
@@ -658,7 +743,7 @@
      );
      // Clear cache after cleanup
      Cache::for('invitations')->flush();
      $this->cache->flush();
   }
   /*****************************************************************
@@ -686,7 +771,7 @@
         $name
      );
      JVB()->email()->sendEmail($email, $subject, $content, 'INVITATION REVOKED');
      Emails::getInstance()->sendEmail($email, $subject, $content, 'INVITATION REVOKED');
   }
   /**