Jake Vanderwerf
2026-08-28 5b50afdcafd150186ea5c7b0e1cad98065590b35
inc/rest/routes/NotificationsRoutes.php
@@ -1,9 +1,11 @@
<?php
namespace JVBase\rest\routes;
use JVBase\Instance;
use JVBase\managers\Cache;
use JVBase\managers\CustomTable;
use JVBase\registrar\Registrar;
use JVBase\managers\NotificationManager;
use JVBase\managers\queue\Queue;
use JVBase\rest\Rest;
use JVBase\rest\Route;
use WP_REST_Request;
@@ -23,6 +25,7 @@
 */
class NotificationsRoutes extends Rest
{
   use Instance;
   protected int $user_id;
   protected array $notification_types = [];
   protected object $manager;
@@ -108,12 +111,6 @@
      $this->cacheTtl = HOUR_IN_SECONDS;
      parent::__construct();
      // Connect notifications cache to user cache
      // When user data changes, notification cache should invalidate
      $this->cache->connect('user');
      $this->notifications = CustomTable::for('notifications');
      $this->metrics = CustomTable::for('notification_metrics');
      // Build complete type map
      $allTypes = [];
@@ -133,7 +130,7 @@
    */
   public function init(): void
   {
      $this->manager = JVB()->notification();
      $this->manager = NotificationManager::getInstance();
      $this->notification_types = $this->manager->getNotificationTypes(true);
   }
@@ -146,7 +143,6 @@
      Route::for('notifications')
         ->get([$this, 'getNotifications'])
         ->args([
            'user' => 'integer|required',
            'type' => 'string',
            'status' => 'string|enum:unread,read,actioned,dismissed',
            'limit' => 'integer|default:20|min:1|max:100',
@@ -160,7 +156,6 @@
      Route::for('notifications/read')
         ->post([$this, 'markRead'])
         ->args([
            'user' => 'integer|required',
            'notification_id' => 'integer|required',
         ])
         ->auth('user')
@@ -171,7 +166,6 @@
      Route::for('notifications/read-all')
         ->post([$this, 'markAllRead'])
         ->args([
            'user' => 'integer|required',
            'type' => 'string',
         ])
         ->auth('user')
@@ -182,7 +176,6 @@
      Route::for('notifications/action')
         ->post([$this, 'markActioned'])
         ->args([
            'user' => 'integer|required',
            'notification_id' => 'integer|required',
         ])
         ->auth('user')
@@ -193,7 +186,6 @@
      Route::for('notifications/dismiss')
         ->post([$this, 'markDismissed'])
         ->args([
            'user' => 'integer|required',
            'notification_id' => 'integer|required',
         ])
         ->auth('user')
@@ -204,7 +196,6 @@
      Route::for('notifications/count')
         ->get([$this, 'getUnreadCount'])
         ->args([
            'user' => 'integer|required',
            'type' => 'string',
         ])
         ->auth('user')
@@ -221,43 +212,13 @@
    */
   public function getNotifications(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      $type = sanitize_text_field($request->get_param('type') ?? '');
      $status = sanitize_text_field($request->get_param('status') ?? '');
      $limit = absint($request->get_param('limit'));
      $offset = absint($request->get_param('offset'));
      if (!$this->checkUser($user_id)) {
      $user_id = get_current_user_id();
      if (!$user_id) {
         return $this->unauthorized();
      }
      $args = $this->buildParams($request);
      $cacheKey = compact('user_id', 'type', 'status', 'limit', 'offset');
      $result = $this->cache->remember($cacheKey, function() use ($user_id, $type, $status, $limit, $offset) {
         // Build where conditions
         $where = ['owner_id' => $user_id];
         if ($type) $where['type'] = $type;
         if ($status) $where['status'] = $status;
         $items = $this->notifications
            ->where($where)
            ->orderBy('created_at', 'DESC')
            ->limit($limit, $offset)
            ->getResults();
         $total = $this->notifications->where($where)->countResults();
         // Format notifications
         $formatted = array_map([$this, 'formatNotification'], $items);
         return [
            'items' => $formatted,
            'total' => $total,
            'has_more' => ($offset + $limit) < $total
         ];
      });
      return $this->success($result);
      return $this->success($this->manager->getUserNotifications($user_id, $args));
   }
   /**
@@ -265,21 +226,14 @@
    */
   public function getUnreadCount(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      $type = sanitize_text_field($request->get_param('type') ?? '');
      if (!$this->checkUser($user_id)) {
      $user_id = get_current_user_id();
      if (!$user_id) {
         return $this->unauthorized();
      }
      $cacheKey = compact('user_id', 'type');
      $count = $this->cache->remember($cacheKey, function() use ($user_id, $type) {
         $where = ['owner_id' => $user_id, 'status' => 'unread'];
         if ($type) $where['type'] = $type;
         return $this->notifications->where($where)->countResults();
      });
      $args = $this->buildParams($request);
      $args['status'] = 'unread';
      $count = $this->manager->countUserNotifications($user_id, $args);
      return $this->success(['count' => $count]);
   }
@@ -381,7 +335,7 @@
                     'action_user_id' => $group->action_user_id,
                     'type' => $group->type
                  ])
                  ->orderBy('created_at', 'DESC')
                  ->orderBy('created_at')
                  ->first();
               if ($notification) {
@@ -403,192 +357,8 @@
      });
   }
   /**
    * Get regular notifications from the notifications table
    */
   protected function getRegularNotifications(int $user_id, array $params): array
   {
      $cacheKey = compact('user_id', 'params');
      return $this->cache->remember($cacheKey, function() use ($user_id, $params) {
         $status = $params['status'];
         $type = $params['type'];
         $limit = $params['limit'];
         $offset = $params['page'];
         // Build base query
         $where = ['owner_id' => $user_id];
         // Handle status filter
         if ($status === 'read') {
            // For multiple statuses, use raw query
            $notifications = $this->getNotificationsWithMultipleStatuses(
               $user_id,
               ['read', 'actioned'],
               $type,
               $limit,
               $offset
            );
         } else {
            // Single status - use fluent builder
            if ($status !== 'all') {
               $where['status'] = $status;
            }
            // Handle type filter
            if ($type !== 'all' && isset($this->typeMap[$type])) {
               $types = $this->typeMap[$type];
               if (!empty($types)) {
                  // Multiple types - use raw query
                  return $this->getNotificationsByTypes(
                     $user_id,
                     $types,
                     $status,
                     $limit,
                     $offset
                  );
               }
            }
            // Simple query - use fluent builder
            $notifications = $this->notifications
               ->where($where)
               ->orderBy('created_at', 'DESC')
               ->limit($limit, $offset)
               ->getResults();
         }
         // Format notifications
         return array_map([$this, 'formatNotification'], $notifications);
      });
   }
   /**
    * Get notifications with multiple status values
    */
   protected function getNotificationsWithMultipleStatuses(
      int $user_id,
      array $statuses,
      string $type,
      int $limit,
      int $offset
   ): array {
      $placeholders = implode(',', array_fill(0, count($statuses), '%s'));
      $params = array_merge([$user_id], $statuses);
      $typeCondition = '';
      if ($type !== 'all' && isset($this->typeMap[$type])) {
         $types = $this->typeMap[$type];
         if (!empty($types)) {
            $typePlaceholders = implode(',', array_fill(0, count($types), '%s'));
            $typeCondition = "AND type IN ({$typePlaceholders})";
            $params = array_merge($params, $types);
         }
      }
      $params[] = $limit;
      $params[] = $offset;
      return $this->notifications->queryResults(
         "SELECT * FROM {table}
             WHERE owner_id = %d
             AND status IN ({$placeholders})
             {$typeCondition}
             ORDER BY created_at DESC
             LIMIT %d OFFSET %d",
         $params
      );
   }
   /**
    * Get notifications by multiple types
    */
   protected function getNotificationsByTypes(
      int $user_id,
      array $types,
      string $status,
      int $limit,
      int $offset
   ): array {
      $placeholders = implode(',', array_fill(0, count($types), '%s'));
      $params = array_merge([$user_id], $types, [$limit, $offset]);
      $statusCondition = '';
      if ($status !== 'all') {
         $statusCondition = "AND status = %s";
         array_splice($params, -2, 0, [$status]);
      }
      return $this->notifications->queryResults(
         "SELECT * FROM {table}
             WHERE owner_id = %d
             AND type IN ({$placeholders})
             {$statusCondition}
             ORDER BY created_at DESC
             LIMIT %d OFFSET %d",
         $params
      );
   }
   /**
    * Get approval notifications from the approval_requests table
    */
   protected function getApprovalNotifications(int $user_id, string $status): array
   {
      $cacheKey = compact('user_id', 'status');
      return $this->cache->remember($cacheKey, function() use ($user_id, $status) {
         if (!$this->isVerifiedUser($user_id)) {
            return [];
         }
         global $wpdb;
         $formatted = [];
         // Build status condition
         $statusCondition = "1=1";
         if ($status === 'unread') {
            $statusCondition = "a.status = 'pending'";
         } elseif ($status === 'read') {
            $statusCondition = "a.status IN ('approved', 'rejected')";
         } elseif ($status !== 'all') {
            $statusCondition = $wpdb->prepare("a.status = %s", $status);
         }
         $approvals = Registrar::getFeatured('approve_new');
         foreach ($approvals as $type => $config) {
            $table = $wpdb->prefix . BASE . 'approval_' . $type . 'requests';
            $votes = $wpdb->prefix . BASE . 'approval_' . $type . 'votes';
            $approvalRequests = $wpdb->get_results(
               $wpdb->prepare(
                  "SELECT a.*,
                        COALESCE(v.vote, 'none') as user_vote
                        FROM {$table} a
                        LEFT JOIN {$votes} v ON a.id = v.request_id AND v.user_id = %d
                        WHERE a.user_id != %d
                        AND {$statusCondition}
                        ORDER BY a.created_at DESC",
                  $user_id,
                  $user_id
               )
            );
            // Filter out requests created by current user
            foreach ($approvalRequests as $approval) {
               $requested_by = json_decode($approval->requested_by, true);
               if (is_array($requested_by) && in_array($user_id, $requested_by)) {
                  continue;
               }
               $formatted[] = $this->formatApprovalNotification($approval);
            }
         }
         return $formatted;
      });
   }
   /**
    * Get content notifications for a user
@@ -647,12 +417,11 @@
    */
   public function markRead(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      $notification_id = absint($request->get_param('notification_id'));
      if (!$this->checkUser($user_id)) {
      $user_id = get_current_user_id();
      if (!$user_id) {
         return $this->unauthorized();
      }
      $notification_id = absint($request->get_param('notification_id'));
      try {
         $result = $this->notifications->transaction(function($table) use ($notification_id, $user_id) {
@@ -695,12 +464,11 @@
    */
   public function markAllRead(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      $type = sanitize_text_field($request->get_param('type') ?? '');
      if (!$this->checkUser($user_id)) {
      $user_id = get_current_user_id();
      if (!$user_id) {
         return $this->unauthorized();
      }
      $type = sanitize_text_field($request->get_param('type') ?? '');
      try {
         $where = ['owner_id' => $user_id, 'status' => 'unread'];
@@ -730,12 +498,11 @@
    */
   public function markActioned(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      $notification_id = absint($request->get_param('notification_id'));
      if (!$this->checkUser($user_id)) {
      $user_id = get_current_user_id();
      if (!$user_id) {
         return $this->unauthorized();
      }
      $notification_id = absint($request->get_param('notification_id'));
      try {
         $result = $this->notifications->transaction(function($table) use ($notification_id, $user_id) {
@@ -779,12 +546,11 @@
    */
   public function markDismissed(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      $notification_id = absint($request->get_param('notification_id'));
      if (!$this->checkUser($user_id)) {
      $user_id = get_current_user_id();
      if (!$user_id) {
         return $this->unauthorized();
      }
      $notification_id = absint($request->get_param('notification_id'));
      try {
         $result = $this->notifications->transaction(function($table) use ($notification_id, $user_id) {
@@ -979,7 +745,7 @@
      }
      if (!empty($queueData)) {
         JVB()->queue()->queueOperation(
         Queue::getInstance()->queueOperation(
            'notification_' . $action,
            $args['user_id'],
            $queueData
@@ -1274,19 +1040,10 @@
    */
   protected function getItemLink(int $ID, string $content): string
   {
      switch ($content) {
         case BASE.'artist':
         case BASE.'artwork':
         case BASE.'event':
         case BASE.'news':
         case BASE.'offer':
         case BASE.'partner':
         case BASE.'piercing':
         case BASE.'tattoo':
            return get_permalink($ID);
         default:
            return get_term_link($ID, BASE.$content);
      }
      return match ($content) {
         BASE . 'artist', BASE . 'artwork', BASE . 'event', BASE . 'news', BASE . 'offer', BASE . 'partner', BASE . 'piercing', BASE . 'tattoo' => get_permalink($ID),
         default => get_term_link($ID, BASE . $content),
      };
   }
   /**
@@ -1314,16 +1071,34 @@
   {
      $params = $request->get_params();
      return [
         'status' => in_array($params['status'] ?? '', ['all', 'unread', 'expired'])
      $args = [
         'status' => in_array($params['status']??'unread', ['all', 'unread', 'expired'])
            ? $params['status']
            : 'unread',
         'user_id' => absint($params['user'] ?? get_current_user_id()),
         'page' => absint($params['page'] ?? 1),
         'type' => in_array($params['type'] ?? '', array_keys($this->manager->notification_types ?? []))
         'type'      => in_array($params['type'] ?? 'all', array_keys($this->manager->notification_types ?? []))
            ? $params['type']
            : 'all',
         'orderby'   => $this->checkArray($params['orderby'], ['target_type', 'priority', 'created_at','read_at','actioned_at','action_taken','status'])??'created_at',
         'order'     => strtoupper($this->checkArray($params['order']??'DESC', ['desc','asc'])??'DESC'),
         'page'      => max(1, absint($params['page']??1)),
         'per_page'  => absint($params['per_page']??20),
      ];
      foreach ($params as $param => $value) {
         $result = match ($param) {
            'target_id'    => absint($value),
            'target_type'  => sanitize_text_field(strtolower($value)),
            'from_user'    => $this->checkUser(absint($value)),
            'priority'     => $this->checkArray($value, ['low','medium', 'high']),
            'action_taken' => $value === 1 ? 1 : 0,
            default => false
         };
         if ($result) {
            $args[$param] = $result;
         }
      }
      return $args;
   }
   /**
@@ -1357,7 +1132,7 @@
         return $irregular[$word];
      }
      if (substr($word, -1) === 'y') {
      if (str_ends_with($word, 'y')) {
         return substr($word, 0, -1) . 'ies';
      }