From 3d25ad7de4fdbb3eba2cde2a62422e6be5d79a0a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Aug 2026 22:57:22 +0000
Subject: [PATCH] =It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh
---
inc/rest/routes/NotificationsRoutes.php | 2908 +++++++++++++++++++++++++++--------------------------------
1 files changed, 1,323 insertions(+), 1,585 deletions(-)
diff --git a/inc/rest/routes/NotificationsRoutes.php b/inc/rest/routes/NotificationsRoutes.php
index cbf36fc..90af2be 100644
--- a/inc/rest/routes/NotificationsRoutes.php
+++ b/inc/rest/routes/NotificationsRoutes.php
@@ -1,1748 +1,1486 @@
<?php
namespace JVBase\rest\routes;
-use JVBase\JVB;
-use JVBase\rest\RestRouteManager;
+use JVBase\Instance;
+use JVBase\managers\Cache;
+use JVBase\managers\CustomTable;
+use JVBase\registrar\Registrar;
+use JVBase\rest\Rest;
+use JVBase\rest\Route;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use Exception;
if (!defined('ABSPATH')) {
- exit; // Exit if accessed directly
+ exit; // Exit if accessed directly
}
/**
- * Step 1: Build status/order/filter params
- * Step 2: Get all regular notifications
- * Step 3: Get all Content notifications
- * Step 4: Get all Approval notifications
- * Step 5: Merge in order of created date
- * Step 6: Return result
+ * Notification Routes Handler
+ *
+ * Manages user notifications including regular notifications, content notifications,
+ * and approval notifications. Provides endpoints for reading, marking, and dismissing.
*/
-class NotificationsRoutes extends RestRouteManager
+class NotificationsRoutes extends Rest
{
- protected int $user_id;
- protected array $notification_types = [];
- protected object $manager;
- protected array $typeMap = [
- 'favourite' => [
- 'new_favourite',
- 'list_shared',
- ],
- 'artist' => [
- 'new_artist',
- 'new_tattoo',
- 'new_piercing',
- 'new_event',
- 'new_update',
+ use Instance;
+ protected int $user_id;
+ protected array $notification_types = [];
+ protected object $manager;
- ],
- 'partner' => [
- 'new_partner',
- 'new_offer',
- ],
- 'shop' => [
- 'new_shop',
- 'shop_update',
- 'shop_accepted',
- 'artist_request',
- ],
- 'event' => [
- 'new_event',
- 'event_reminder',
- ],
- 'news' => [
- 'new_update',
- ],
- 'system' => [
- 'system_message',
- 'artist_approved',
- 'artist_invitation',
- 'artist_request',
- 'shop_accepted',
- 'shop_rejected',
- 'new_term',
- 'term_approved',
- 'term_rejected',
- ],
- ];
- protected array $notificationTableMap = [
- // Regular notifications
- 'notifications' => [
- 'new_favourite',
- 'artist_approved',
- 'artist_rejected',
- 'artist_invitation',
- 'shop_invitation',
- 'artist_request',
- 'shop_accepted',
- 'shop_rejected',
- 'new_term',
- 'term_approved',
- 'term_rejected',
- 'list_shared',
- 'system_message'
- ],
+ protected array $typeMap = [
+ 'favourite' => [
+ 'new_favourite',
+ 'list_shared',
+ ],
+ 'artist' => [
+ 'new_artist',
+ 'new_tattoo',
+ 'new_piercing',
+ 'new_event',
+ 'new_update',
+ ],
+ 'partner' => [
+ 'new_partner',
+ 'new_offer',
+ ],
+ 'shop' => [
+ 'new_shop',
+ 'shop_update',
+ 'shop_accepted',
+ 'artist_request',
+ ],
+ 'event' => [
+ 'new_event',
+ 'event_reminder',
+ ],
+ 'news' => [
+ 'new_update',
+ ],
+ 'system' => [
+ 'system_message',
+ 'artist_approved',
+ 'artist_invitation',
+ 'artist_request',
+ 'shop_accepted',
+ 'shop_rejected',
+ 'new_term',
+ 'term_approved',
+ 'term_rejected',
+ ],
+ ];
- // Content notifications (from artists the user follows)
- 'content_notifications' => [
- 'new_artist',
- 'new_tattoo',
- 'new_piercing',
- 'new_event',
- 'new_update',
- 'new_partner',
- 'new_offer',
- 'new_shop',
- 'shop_update',
- 'event_reminder'
- ]
- ];
+ protected array $notificationTableMap = [
+ 'notifications' => [
+ 'new_favourite',
+ 'artist_approved',
+ 'artist_rejected',
+ 'artist_invitation',
+ 'shop_invitation',
+ 'artist_request',
+ 'shop_accepted',
+ 'shop_rejected',
+ 'new_term',
+ 'term_approved',
+ 'term_rejected',
+ 'list_shared',
+ 'system_message'
+ ],
+ 'content_notifications' => [
+ 'new_artist',
+ 'new_tattoo',
+ 'new_piercing',
+ 'new_event',
+ 'new_update',
+ 'new_partner',
+ 'new_offer',
+ 'new_shop',
+ 'shop_update',
+ 'event_reminder'
+ ]
+ ];
- public function __construct()
- {
- $this->cache_name = 'notifications';
- parent::__construct();
+ protected CustomTable $notifications;
+ protected CustomTable $metrics;
- $allTypes = [];
- foreach ($this->typeMap as $key => $values) {
- $allTypes = array_unique(array_merge($allTypes, $values));
- }
+ public function __construct()
+ {
+ $this->cacheName = 'notifications';
+ $this->cacheTtl = HOUR_IN_SECONDS;
+ parent::__construct();
- $this->typeMap['all'] = $allTypes;
+ // Connect notifications cache to user cache
+ // When user data changes, notification cache should invalidate
+ $this->cache->connect('user');
- $this->user_id = get_current_user_id();
- $this->action = 'notifications-';
+ $this->notifications = CustomTable::for('notifications');
+ $this->metrics = CustomTable::for('notification_metrics');
- add_action('init', [$this, 'init']);
+ // Build complete type map
+ $allTypes = [];
+ foreach ($this->typeMap as $key => $values) {
+ $allTypes = array_unique(array_merge($allTypes, $values));
+ }
+ $this->typeMap['all'] = $allTypes;
- add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
- }
- /**
- * Format a notification for display
- *
- * @param object $notification Notification object
- *
- * @return array Formatted notification
- */
- protected function formatNotification(object $notification):array
- {
- $config = $this->notification_types[$notification->type] ?? [];
- $context = json_decode($notification->context ?? '{}', true);
+ $this->user_id = get_current_user_id();
- // Get action user's name if available
- $acting_user_name = null;
- if ($notification->action_user_id) {
- $acting_user_name = jvbShareName($notification->action_user_id);
- }
+ add_action('init', [$this, 'init']);
+ add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
+ }
- return [
- 'id' => $notification->id,
- 'type' => $notification->type,
- 'message' => $notification->message,
- 'created_at' => $notification->created_at,
- 'status' => $notification->status,
- 'requires_action' => (bool)$notification->requires_action,
- 'action_taken' => (bool)$notification->action_taken,
- 'icon' => $config['icon'] ?? 'info',
- 'priority' => $notification->priority,
- 'target' => [
- 'id' => $notification->target_id,
- 'type' => $notification->target_type
- ],
- 'context' => $context,
- 'acting_user' => $notification->action_user_id ? [
- 'id' => $notification->action_user_id,
- 'name' => $acting_user_name
- ] : null
- ];
- }
+ /**
+ * Set up required parameters
+ */
+ public function init(): void
+ {
+ $this->manager = NotificationManager::getInstance();
+ $this->notification_types = $this->manager->getNotificationTypes(true);
+ }
- /**
- * Set up required paramaters
- * @return void
- */
- public function init()
- {
- $this->manager = JVB()->notification();
- $this->notification_types = $this->manager->getNotificationTypes();
- }
+ /**
+ * Register notification routes
+ */
+ public function registerRoutes(): void
+ {
+ // Get user notifications
+ Route::for('notifications')
+ ->get([$this, 'getNotifications'])
+ ->args([
+ 'type' => 'string',
+ 'status' => 'string|enum:unread,read,actioned,dismissed',
+ 'limit' => 'integer|default:20|min:1|max:100',
+ 'offset' => 'integer|default:0',
+ ])
+ ->auth('user')
+ ->rateLimit(30)
+ ->register();
- /**
- * Registers notification routes
- * @return void
- */
- public function registerRoutes():void
- {
- register_rest_route($this->namespace, '/notifications', [
- [
- 'methods' => 'GET',
- 'callback' => [$this, 'getNotifications'],
- 'permission_callback' => [$this, 'checkPermission']
- ],
- [
- 'methods' => 'POST',
- 'callback' => [$this, 'updateNotifications'],
- 'permission_callback' => [$this, 'checkPermission']
- ]
- ]);
- }
+ // Mark as read
+ Route::for('notifications/read')
+ ->post([$this, 'markRead'])
+ ->args([
+ 'notification_id' => 'integer|required',
+ ])
+ ->auth('user')
+ ->rateLimit(30)
+ ->register();
+ // Mark all as read
+ Route::for('notifications/read-all')
+ ->post([$this, 'markAllRead'])
+ ->args([
+ 'type' => 'string',
+ ])
+ ->auth('user')
+ ->rateLimit(10)
+ ->register();
- /**
- * @param int $ID
- * @param string $content
- *
- * @return string
- */
- protected function getItemLink(int $ID, string $content):string
- {
- error_log('Type: '.print_r($content, true));
- 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);
- }
- }
+ // Mark as actioned
+ Route::for('notifications/action')
+ ->post([$this, 'markActioned'])
+ ->args([
+ 'notification_id' => 'integer|required',
+ ])
+ ->auth('user')
+ ->rateLimit(30)
+ ->register();
- /**
- * Get notification actions
- *
- * @param string $type Notification type
- * @param array $data Notification data
- * @param object $notification Full notification object
- *
- * @return array Actions available for this notification
- */
- protected function getNotificationActions(string $type, array $data, object $notification = null):array
- {
- error_log('Data for actions: '.print_r($data, true));
+ // Dismiss notification
+ Route::for('notifications/dismiss')
+ ->post([$this, 'markDismissed'])
+ ->args([
+ 'notification_id' => 'integer|required',
+ ])
+ ->auth('user')
+ ->rateLimit(30)
+ ->register();
- $actions = [];
+ // Get unread count
+ Route::for('notifications/count')
+ ->get([$this, 'getUnreadCount'])
+ ->args([
+ 'type' => 'string',
+ ])
+ ->auth('user')
+ ->rateLimit()
+ ->register();
+ }
- switch ($type) {
- case 'artist_approved':
- case 'artist_rejected':
- case 'shop_approved':
- case 'shop_rejected':
- case 'term_approved':
- case 'term_rejected':
- case 'system_message':
- //No extra action needed
- break;
- case 'artist_invitation':
- $actions[] = [
- 'icon' => 'upvote',
- 'label' => 'Approve',
- 'action' => 'acceptInvitation',
- ];
- $actions[] = [
- 'icon' => 'downvote',
- 'label' => 'Reject',
- 'action' => 'reject_invitation',
- ];
- break;
- case 'artist_request':
- $actions[] = [
- 'icon' => 'upvote',
- 'label' => 'Approve',
- 'action' => 'accept_to_shop',
- ];
- $actions[] = [
- 'icon' => 'downvote',
- 'label' => 'Reject',
- 'action' => 'reject_to_shop',
- ];
- break;
- case 'artist_approval':
- $actions[] = [
- 'icon' => 'upvote',
- 'label' => 'Approve',
- 'action' => 'accept_artist',
- ];
- $actions[] = [
- 'icon' => 'downvote',
- 'label' => 'Reject',
- 'action' => 'reject_artist',
- ];
- break;
+ // =========================================================================
+ // GET OPERATIONS
+ // =========================================================================
- case 'new_term':
- $actions[] = [
- 'icon' => 'upvote',
- 'label' => 'Approve',
- 'action' => 'approve_term',
- ];
- $actions[] = [
- 'icon' => 'downvote',
- 'label' => 'Reject',
- 'action' => 'reject_term',
- ];
- break;
+ /**
+ * Get notifications for a user
+ */
+ public function getNotifications(WP_REST_Request $request): WP_REST_Response
+ {
+ $user_id = get_current_user_id();
+ if (!$user_id) {
+ return $this->unauthorized();
+ }
+ $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'));
- case 'list_shared':
- if (!empty($data['list_id'])) {
- $actions[] = [
- 'icon' => 'list-heart',
- 'label' => 'View List',
- 'url' => home_url("/dash/favourites/{$data['list_id']}"),
- ];
- }
- break;
- default:
- $actions[] = [
- 'icon' => 'link',
- 'label' => 'View',
- 'url' => $this->getItemLink($data['target_id'], $data['target_type']),
- ];
- break;
- }
+ $cacheKey = compact('user_id', 'type', 'status', 'limit', 'offset');
- $actions[] = [
- 'icon' => 'close',
- 'label' => 'Dismiss',
- 'action' => 'dismiss_notification'
- ];
- // Allow customization via filter
- return apply_filters('jvb_notification_actions', $actions, $type, $data, $notification);
- }
+ $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;
- /**
- * @param int $user_id
- * @param array $data
- *
- * @return array
- */
- protected function getSanitizedData(int $user_id, array $data):array
- {
- $status = (array_key_exists('status', $data)) ? $data['status'] : 'unread';
- $limit = (array_key_exists('limit', $data)) ? $data['limit'] : 20;
- $offset = (array_key_exists('page', $data)) ? $data['page'] : 1;
- $type = (array_key_exists('type', $data)) ? $data['type'] : 'all';
-
- // Validate and sanitize status
- $allowed_statuses = ['unread', 'read', 'actioned', 'dismissed', 'all'];
- if (!in_array($status, $allowed_statuses)) {
- $this->logError("Invalid notification status", [
- 'status' => $status,
- 'user' => $user_id
- ], 'warning');
- $status = 'unread'; // Default to unread if invalid
- }
-
- if (!in_array($type, array_keys($this->typeMap))) {
- $this->logError("Invalid notification type", [
- 'type' => $type,
- 'user' => $user_id
- ], 'warning');
- $type = 'all';
- }
-
- // Validate and sanitize limit and offset
- $limit = absint($limit);
- if ($limit <= 0 || $limit > 100) {
- $limit = 20; // Use reasonable default if invalid
- }
-
- $offset = absint($offset);
- if ($offset < 0) {
- $offset = 1;
- }
-
- $return = [
- 'status' => $status,
- 'limit' => $limit,
- 'page' => $offset,
- 'type' => $type
- ];
- if (array_key_exists('grouped', $data)) {
- $return['grouped'] = $data['grouped'];
- }
- return $return;
- }
- /**
- * Get notifications for a user
- *
- * @param WP_REST_Request $request
- * @return WP_REST_Response
- */
- public function getNotifications(WP_REST_Request $request): WP_REST_Response
- {
- $data = $request->get_params();
- $user_id = $data['user'];
- if (!$this->userCheck($user_id)) {
- $this->logError("Invalid user ID for notifications", [
- 'user' => $user_id
- ], 'warning');
- return new WP_REST_Response([
- 'success' => false,
- 'message' => 'User doesn\'t match. Are you a bot?'
- ]);
- }
-
- // Step 1: Build status/order/filter params
- $params = $this->getSanitizedData($user_id, $data);
- $status = $params['status'];
- $limit = $params['limit'];
- $offset = $params['page'];
- $type = $params['type'];
-
- // Try cache first with validated parameters
- $cache_key = "user_{$user_id}_merged_notifications_{$status}_{$type}_{$limit}_{$offset}";
- $cached = $this->cache->get($cache_key);
- if ($cached) {
- return new WP_REST_Response($cached);
- }
-
- try {
- // Step 2: Get regular notifications
- $regular_notifications = $this->getRegularNotifications($user_id, $params);
-
- // Step 3: Get content notifications
- $content_notifications = $this->getContentNotifications($user_id, $status, $limit, $offset);
-
- // Step 4: Get approval notifications
- $approval_notifications = $this->getApprovalNotifications($user_id, $status);
-
- // Step 5: Merge in order of created date
- $notifications = array_merge(
- $regular_notifications,
- $content_notifications,
- $approval_notifications
- );
-
- usort($notifications, function ($a, $b) {
- $date_a = strtotime($a['created_at'] ?? $a['date'] ?? date('Y-m-d H:i:s'));
- $date_b = strtotime($b['created_at'] ?? $b['date'] ?? date('Y-m-d H:i:s'));
- return $date_b - $date_a; // Sort from newest to oldest
- });
-
- // Apply pagination
- $total_count = count($notifications);
- $notifications = array_slice($notifications, 0, $limit);
-
- // Step 6: Return result
- $response = [
- 'notifications' => $notifications,
- 'pagination' => [
- 'total' => $total_count,
- 'page' => $offset,
- 'per_page' => $limit,
- 'pages' => ceil($total_count / $limit),
- 'has_more' => ($offset * $limit + count($notifications)) < $total_count
- ]
- ];
-
- // Cache the result
- $this->cache->set($cache_key, $response, 'notifications_' . $user_id);
- return new WP_REST_Response($response);
- } catch (Exception $e) {
- $this->logError("Error retrieving notifications", [
- 'user_id' => $user_id,
- 'error' => $e->getMessage()
- ]);
-
- return new WP_REST_Response([
- 'notifications' => [],
- 'pagination' => [
- 'total' => 0,
- 'page' => $offset,
- 'per_page' => $limit,
- 'pages' => 0,
- 'has_more' => false
- ]
- ]);
- }
- }
-
- /**
- * Get regular notifications from the notifications table
- *
- * @param int $user_id User ID
- * @param array $params Filter parameters
- * @return array Array of formatted notifications
- */
- protected function getRegularNotifications(int $user_id, array $params): array
- {
- $status = $params['status'];
- $limit = $params['limit'];
- $offset = $params['page'];
- $type = $params['type'];
-
- // Try to get from cache first with validated parameters
- $cache_key = "user_{$user_id}_regular_notifications_{$status}_{$type}_{$limit}_{$offset}";
- $cached = $this->cache->get($cache_key);
- if ($cached) {
- return $cached;
- }
-
- global $wpdb;
- $notifications_table = $wpdb->prefix . BASE . 'notifications';
-
- // Build status condition
- $status_condition = "1=1";
- if ($status === 'unread') {
- $status_condition = "status = 'unread'";
- } elseif ($status === 'read') {
- $status_condition = "status IN ('read', 'actioned')";
- } elseif ($status !== 'all') {
- $status_condition = $wpdb->prepare("status = %s", $status);
- }
-
- // Build type condition
- $type_condition = "1=1";
- if ($type !== 'all' && isset($this->typeMap[$type])) {
- $types = $this->typeMap[$type];
- if (!empty($types)) {
- $placeholders = implode(',', array_fill(0, count($types), '%s'));
- $type_condition = $wpdb->prepare("type IN ($placeholders)", $types);
- }
- }
-
- // Get notifications
- $notifications = $wpdb->get_results(
- $wpdb->prepare(
- "SELECT * FROM {$notifications_table}
- WHERE owner_id = %d AND {$status_condition} AND {$type_condition}
- ORDER BY created_at DESC",
- $user_id
- )
- );
-
- // Format notifications
- $formatted = [];
- foreach ($notifications as $notification) {
- $formatted[] = $this->formatNotification($notification);
- }
-
- // Cache the results
- $this->cache->set($cache_key, $formatted, 'notifications_' . $user_id);
-
- return $formatted;
- }
-
- /**
- * Get approval notifications from the approval_requests table
- *
- * @param int $user_id User ID
- * @param string $status Filter by status
- * @return array Array of formatted approval notifications
- */
- protected function getApprovalNotifications(int $user_id, string $status): array
- {
- // Try to get from cache first
- $cache_key = "user_{$user_id}_approval_notifications_{$status}";
- $cached = $this->cache->get($cache_key);
-
- if ($cached) {
- return $cached;
- }
-
- global $wpdb;
- $formatted = [];
-
- // Build status condition
- $status_condition = "1=1";
- if ($status === 'unread') {
- $status_condition = "a.status = 'pending'";
- } elseif ($status === 'read') {
- $status_condition = "a.status IN ('approved', 'rejected')";
- } elseif ($status !== 'all') {
- $status_condition = $wpdb->prepare("a.status = %s", $status);
- }
-
- if ($this->isVerifiedUser($user_id)) {
- $approvals = jvbApprovalTypes();
- foreach ($approvals as $type => $config) {
- $table = $wpdb->prefix.BASE.'approval_'.$type.'requests';
- $votes = $wpdb->prefix.BASE.'approval_'.$type.'votes';
-
- $approvals = $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 {$status_condition}
- ORDER BY a.created_at DESC",
- $user_id,
- $user_id
- )
- );
- // Now filter out requests created by the current user
- foreach ($approvals as $approval) {
- $requested_by = json_decode($approval->requested_by, true);
-
- // Skip if the current user is the requester
- if (is_array($requested_by) && in_array($user_id, $requested_by)) {
- continue;
- }
+ $items = $this->notifications
+ ->where($where)
+ ->orderBy('created_at', 'DESC')
+ ->limit($limit, $offset)
+ ->getResults();
- $formatted[] = $this->formatApprovalNotification($approval);
- }
- }
- }
+ $total = $this->notifications->where($where)->countResults();
- // Cache the results
- $this->cache->set($cache_key, $formatted, 'approvals');
+ // Format notifications
+ $formatted = array_map([$this, 'formatNotification'], $items);
- return $formatted;
- }
+ return [
+ 'items' => $formatted,
+ 'total' => $total,
+ 'has_more' => ($offset + $limit) < $total
+ ];
+ });
- /**
- * Format an approval request as a notification
- *
- * @param object $approval Approval request object
- * @return array Formatted notification
- */
- protected function formatApprovalNotification(object $approval): array
- {
- $data = json_decode($approval->data ?? '{}', true);
- $type_labels = [
- 'artist_approval' => 'Artist Verification',
- 'term_suggestion' => 'Term Suggestion'
- ];
+ return $this->success($result);
+ }
- $status_labels = [
- 'pending' => 'Pending',
- 'approved' => 'Approved',
- 'rejected' => 'Rejected',
- 'expired' => 'Expired'
- ];
+ /**
+ * Get unread notification count
+ */
+ public function getUnreadCount(WP_REST_Request $request): WP_REST_Response
+ {
+ $user_id = get_current_user_id();
+ if (!$user_id) {
+ return $this->unauthorized();
+ }
+ $type = sanitize_text_field($request->get_param('type') ?? '');
- $icon = ($approval->type === 'artist_approval') ? 'artist' : 'style';
+ $cacheKey = compact('user_id', 'type');
- $message = '';
- if ($approval->type === 'artist_approval') {
- if ($approval->requested_by == $approval->target_id) {
- $message = "Your artist verification is {$status_labels[$approval->status]}";
- } else {
- $name = isset($data['display_name']) ? $data['display_name'] : 'An artist';
- $message = "{$name} is requesting verification";
- }
- } elseif ($approval->type === 'term_suggestion') {
- $term_name = $data['term_name'] ?? 'A term';
- $taxonomy = $data['taxonomy'] ?? '';
- $taxonomy_name = str_replace(BASE, '', $taxonomy);
+ $count = $this->cache->remember($cacheKey, function() use ($user_id, $type) {
+ $where = ['owner_id' => $user_id, 'status' => 'unread'];
+ if ($type) $where['type'] = $type;
- if ($approval->requested_by == get_current_user_id()) {
- $message = "Your {$taxonomy_name} suggestion '{$term_name}' is {$status_labels[$approval->status]}";
- } else {
- $message = "New {$taxonomy_name} suggestion: '{$term_name}'";
- }
- }
+ return $this->notifications->where($where)->countResults();
+ });
- return [
- 'id' => 'approval_' . $approval->id,
- 'type' => $approval->type,
- 'message' => $message,
- 'created_at' => $approval->created_at,
- 'status' => $approval->status,
- 'requires_action' => ($approval->status === 'pending' && $approval->requested_by != get_current_user_id()),
- 'action_taken' => !empty($approval->user_vote) && $approval->user_vote !== 'none',
- 'icon' => $icon,
- 'priority' => 'high',
- 'target' => [
- 'id' => $approval->target_id,
- 'type' => $approval->target_type
- ],
- 'context' => $data,
- 'approval_data' => [
- 'required_approvals' => $approval->required_approvals,
- 'current_approvals' => $approval->current_approvals,
- 'expires_at' => $approval->expires_at
- ]
- ];
- }
+ return $this->success(['count' => $count]);
+ }
- /**
- * Determine which table a notification type belongs to
- *
- * @param string $notificationType The notification type
- * @return string The table name ('notifications' or 'content_notifications')
- */
- protected function getTableForNotificationType(string $notificationType): string
- {
- foreach ($this->notificationTableMap as $table => $types) {
- if (in_array($notificationType, $types)) {
- return $table;
- }
- }
- // Default to the main notifications table if type is unknown
- return 'notifications';
- }
+ /**
+ * Get grouped notifications for a user
+ */
+ public function getGroupedNotifications(
+ int $user_id,
+ string $status,
+ int $limit,
+ int $offset,
+ string $type
+ ): array {
+ $cacheKey = compact('user_id', 'status', 'limit', 'offset', 'type');
- /**
- * Get grouped notifications for a user
- * @param int $user_id User ID
- * @param string $status notification status
- * @param int $limit number of notifications to fetch
- * @param int $offset page to fetch
- * @param string $type notification type to fetch
- * @return array Grouped notifications with pagination info
- */
- public function getGroupedNotifications(int $user_id, string $status, int $limit, int $offset, string $type):array
- {
- $cache_key = "user_{$user_id}_grouped_notifications_{$status}_{$type}_{$limit}_{$offset}";
- $cached = $this->cache->get($cache_key);
- if ($cached !== false) {
- return $cached;
- }
+ return $this->cache->remember($cacheKey, function() use ($user_id, $status, $limit, $offset, $type) {
+ $time_window = '24 HOUR';
- global $wpdb;
+ // Build type filter
+ $typeFilter = '';
+ $typeValues = [];
- // Build status condition
- $status_condition = "1=1";
- if ($status === 'unread') {
- $status_condition = "status = 'unread'";
- } elseif ($status === 'read') {
- $status_condition = "status IN ('read', 'actioned')";
- } elseif ($status !== 'all') {
- $status_condition = $wpdb->prepare("status = %s", $status);
- }
+ if ($type !== 'all' && isset($this->typeMap[$type])) {
+ $types = $this->typeMap[$type];
+ if (!empty($types)) {
+ $placeholders = implode(',', array_fill(0, count($types), '%s'));
+ $typeFilter = "AND type IN ({$placeholders})";
+ $typeValues = $types;
+ }
+ }
- // Build type condition
- $type_condition = "1=1";
- if ($type !== 'all' && isset($this->typeMap[$type])) {
- $types = $this->typeMap[$type];
- if (!empty($types)) {
- $placeholders = implode(',', array_fill(0, count($types), '%s'));
- $type_condition = $wpdb->prepare("type IN ($placeholders)", $types);
- }
- }
- try {
- // Time window for grouping (e.g., last 24 hours)
- $time_window = '24 HOUR';
+ // Build status filter
+ $statusFilter = '';
+ if ($status === 'read') {
+ $statusFilter = "AND status IN ('read', 'actioned')";
+ } elseif ($status !== 'all') {
+ $statusFilter = "AND status = %s";
+ $typeValues[] = $status;
+ }
- $table = $wpdb->prefix.BASE.'notifications';
- // Count notifications by action_user_id and type
- $grouped_counts = $wpdb->get_results(
- $wpdb->prepare(
- "SELECT
+ // Get grouped notifications
+ $grouped = $this->notifications->queryResults(
+ "SELECT
action_user_id,
type,
COUNT(*) as count,
MAX(created_at) as latest_time,
MIN(id) as first_id,
GROUP_CONCAT(target_type) as target_types
- FROM {$table}
+ FROM {table}
WHERE owner_id = %d
AND action_user_id IS NOT NULL
- AND {$status_condition}
- AND {$type_condition}
AND created_at > DATE_SUB(NOW(), INTERVAL {$time_window})
+ {$statusFilter}
+ {$typeFilter}
GROUP BY action_user_id, type
ORDER BY latest_time DESC
LIMIT %d OFFSET %d",
- $user_id,
- $limit,
- $offset
- )
- );
+ array_merge([$user_id], $typeValues, [$limit, $offset])
+ );
-
- // Get total count for pagination
- $total_count = $wpdb->get_var(
- $wpdb->prepare(
- "SELECT COUNT(DISTINCT CONCAT(action_user_id, '_', type))
- FROM {$table}
+ // Get total count
+ $total = $this->notifications->queryVar(
+ "SELECT COUNT(DISTINCT CONCAT(action_user_id, '_', type))
+ FROM {table}
WHERE owner_id = %d
AND action_user_id IS NOT NULL
- AND {$status_condition}
- AND {$type_condition}
- AND created_at > DATE_SUB(NOW(), INTERVAL {$time_window})",
- $user_id
- )
- );
+ AND created_at > DATE_SUB(NOW(), INTERVAL {$time_window})
+ {$statusFilter}
+ {$typeFilter}",
+ array_merge([$user_id], $typeValues)
+ );
- // Format the grouped notifications
- $formatted = [];
- foreach ($grouped_counts as $group) {
- // Get acting user name
- $acting_user_name = jvbGetUsername($group->action_user_id);
+ // Format results
+ $formatted = [];
+ foreach ($grouped as $group) {
+ if ($group->count > 1) {
+ // Grouped notification
+ $target_types = explode(',', $group->target_types);
+ $formatted[] = [
+ 'id' => 'group_' . $group->first_id,
+ 'type' => $group->type,
+ 'message' => $this->buildGroupedMessage(
+ jvbGetUsername($group->action_user_id),
+ $group->type,
+ $group->count,
+ $target_types
+ ),
+ 'created_at' => $group->latest_time,
+ 'is_grouped' => true,
+ 'group_count' => $group->count,
+ ];
+ } else {
+ // Single notification
+ $notification = $this->notifications
+ ->where([
+ 'owner_id' => $user_id,
+ 'action_user_id' => $group->action_user_id,
+ 'type' => $group->type
+ ])
+ ->orderBy('created_at', 'DESC')
+ ->first();
- if ($group->count > 1) {
- // Get unique target types for better message formatting
- $target_types = array_unique(explode(',', $group->target_types));
+ if ($notification) {
+ $formatted[] = $this->formatNotification($notification);
+ }
+ }
+ }
- // Build a grouped notification
- $message = $this->buildGroupedMessage(
- $acting_user_name,
- $group->type,
- $group->count,
- $target_types
- );
+ return [
+ 'notifications' => $formatted,
+ 'pagination' => [
+ 'total' => (int)$total,
+ 'page' => $offset,
+ 'per_page' => $limit,
+ 'pages' => ceil($total / $limit)
+ ],
+ 'has_more' => ($offset + $limit) < $total
+ ];
+ });
+ }
- $formatted[] = [
- 'id' => 'group_' . $group->first_id,
- 'type' => $group->type,
- 'message' => $message,
- 'created_at' => $group->latest_time,
- 'timestamp' => strtotime($group->latest_time),
- 'status' => $status,
- 'icon' => $this->notification_types[$group->type]['icon'] ?? 'info',
- 'priority' => $this->notification_types[$group->type]['priority'] ?? 'normal',
- 'is_grouped' => true,
- 'group_count' => $group->count,
- 'acting_user' => [
- 'id' => $group->action_user_id,
- 'name' => $acting_user_name
- ],
- 'target_types' => $target_types
- ];
- } else {
- // Get the single notification details
- $table = $wpdb->prefix.BASE.'notifications';
- $notification = $wpdb->get_row(
- $wpdb->prepare(
- "SELECT * FROM {$table}
- WHERE owner_id = %d
- AND action_user_id = %d
- AND type = %s
- ORDER BY created_at DESC
- LIMIT 1",
- $user_id,
- $group->action_user_id,
- $group->type
- )
- );
+ /**
+ * Get regular notifications from the notifications table
+ */
+ protected function getRegularNotifications(int $user_id, array $params): array
+ {
+ $cacheKey = compact('user_id', 'params');
- if ($notification) {
- $formatted[] = $this->formatNotification($notification);
- }
- }
- }
+ return $this->cache->remember($cacheKey, function() use ($user_id, $params) {
+ $status = $params['status'];
+ $type = $params['type'];
+ $limit = $params['limit'];
+ $offset = $params['page'];
- // Prepare response with pagination info
- $response = [
- 'notifications' => $formatted,
- 'pagination' => [
- 'total' => (int)$total_count,
- 'page' => $offset,
- 'per_page' => $limit,
- 'pages' => ceil($total_count / $limit),
- 'has_more' => ($offset + $limit) < $total_count
- ]
- ];
+ // Build base query
+ $where = ['owner_id' => $user_id];
- // Cache the results
- $this->cache->set($cache_key, $response, 'notifications_' . $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;
+ }
- return $response;
- } catch (Exception $e) {
- $this->logError("Error retrieving grouped notifications", [
- 'user_id' => $user_id,
- 'status' => $status,
- 'error' => $e->getMessage()
- ]);
+ // 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
+ );
+ }
+ }
- return [
- 'notifications' => [],
- 'pagination' => [
- 'total' => 0,
- 'page' => $offset,
- 'per_page' => $limit,
- 'pages' => 0,
- 'has_more' => false
- ]
- ];
- }
- }
+ // Simple query - use fluent builder
+ $notifications = $this->notifications
+ ->where($where)
+ ->orderBy('created_at', 'DESC')
+ ->limit($limit, $offset)
+ ->getResults();
+ }
- /**
- * Build a message for grouped notifications
- *
- * @param string $user_name Acting user's name
- * @param string $type Notification type
- * @param int $count Number of grouped notifications
- * @param array $target_types Types of targets involved
- * @return string Formatted message
- */
- protected function buildGroupedMessage(string $user_name, string $type, int $count, array $target_types = []):string
- {
- switch ($type) {
- case 'new_favourite':
- // If we have a single target type
- if (count($target_types) === 1) {
- $content_type = $this->manager->getContentTypeLabel($target_types[0]);
- return "{$user_name} favourited {$count} of your {$content_type}";
- }
- return "{$user_name} favourited {$count} of your items";
+ // Format notifications
+ return array_map([$this, 'formatNotification'], $notifications);
+ });
+ }
- case 'artist_request':
- return "{$user_name} wants to join your shop";
+ /**
+ * 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);
- // Add more cases for other notification types
- default:
- return "{$user_name} has {$count} notifications for you";
- }
- }
+ $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;
- /**
- * Build Notification request
- *
- * @param WP_REST_Request $request
- * @return array Sanitized parameters for checking the cache
- */
- protected function buildParams(WP_REST_Request $request):array
- {
- $request = $request->get_params();
- return [
- 'status' => (array_key_exists('status', $request) && in_array($request['status'], ['all', 'unread', 'expired'])) ? $request['status'] : 'unread',
- 'user_id' => (array_key_exists('user', $request) && is_int($request['user'])) ? $request['user'] : get_current_user_id(),
- 'page' => (array_key_exists('page', $request) && is_numeric($request['page'])) ? $request['page'] : 1,
- 'type' => (array_key_exists('type', $request) && in_array($request['type'], array_keys($this->manager->notification_types))) ? $request['type'] : 'all',
- ];
- }
+ 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
+ );
+ }
- /**
- * @param WP_REST_Request $request
- *
- * @return WP_REST_Response
- */
- public function updateNotifications(WP_REST_Request $request):WP_REST_Response
- {
- $data = $request->get_params();
- $action = $request->get_param('action');
- $notificationID = (array_key_exists('notification', $data) && is_int($data['notification'])) ? $data['notification'] : false;
- $args = $this->buildParams($request);
- $data = [];
- $error = '';
- switch ($action) {
- case 'mark_as_read':
- if (is_null($notificationID)) {
- return new WP_REST_Response([
- 'success' => false,
- 'message' => 'Notification ID is required'
- ]);
- }
- $data = [
- 'user_id' => $args['user_id'],
- 'notification_id' => $notificationID,
- ];
- break;
- case 'mark_all_as_read':
- $data = [
- 'user_id' => $args['user_id'],
- ];
- if ($request->get_param('notification_ids')) {
- //To bulk mark all ids
- $data['notification_ids'] = array_map('intval', $request->get_param('notification_ids'));
- }
- if ($request->get_param('type')) {
- //To bulk select all items by type
- $data['type'] = $request->get_param('type');
- }
- break;
- case 'approve_artist':
- case 'reject_artist':
- //TODO: hook into the approval routes already set up
- $handler = JVB()->routes('approvals');
- $ID = $handler->getVerificationDetails($args['user_id']);
- $notes = isset($request['notes']) ? sanitize_text_field($request['notes']) : '';
- $handler->voteForArtist($args['user_id'], $ID['request'], $action==='approve_artist', $notes);
+ /**
+ * 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]);
- $this->markActioned($notificationID, $args['user_id']);
- break;
- case 'acceptInvitation':
- case 'reject_invitation':
- //TODO: hook into the shop routes already set up
- $handler = JVB()->routes('shop');
- $notification_data = $this->getNotification($notificationID);
+ $statusCondition = '';
+ if ($status !== 'all') {
+ $statusCondition = "AND status = %s";
+ array_splice($params, -2, 0, [$status]);
+ }
- if ($action === 'acceptInvitation') {
- $result = $handler->acceptShopInvitation($args['user_id'], $args['shop_id']);
- } else {
- $result = $handler->declineShopInvitation($args['user_id'], $args['shop_id']);
- }
+ 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
+ );
+ }
- $this->markActioned($notificationID, $args['user_id']);
- break;
- case 'accept_to_shop':
- case 'reject_to_shop':
- $handler = JVB()->routes('shop');
+ /**
+ * Get approval notifications from the approval_requests table
+ */
+ protected function getApprovalNotifications(int $user_id, string $status): array
+ {
+ $cacheKey = compact('user_id', 'status');
- if ($action === 'accept_to_shop') {
- $result = $handler->addArtistToShop($args['user_id'], $args['shop_id']);
- } else {
- //TODO: notify requester that their request has been denied
- }
+ return $this->cache->remember($cacheKey, function() use ($user_id, $status) {
+ if (!$this->isVerifiedUser($user_id)) {
+ return [];
+ }
- $this->markActioned($notificationID, $args['user_id']);
- break;
- case 'approve_term':
- case 'reject_term':
- $handler = JVB()->routes('approvals');
- $notification_data = $this->getNotification($notificationID);
- $notes = isset($request['notes']) ? sanitize_text_field($request['notes']) : '';
+ global $wpdb;
+ $formatted = [];
- if ($action === 'approve_term') {
- $result = $handler->approveTerm($args['user_id'], $notification_data, $notes);
- } else {
- $result = $handler->rejectTerm($args['user_id'], $notification_data, $notes);
- }
+ // 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);
+ }
- $this->markActioned($notificationID, $args['user_id']);
- break;
- case 'dismiss_notification':
- $data = [
- 'user_id' => $args['user_id'],
- 'notification_id' => $notificationID,
- ];
- break;
+ $approvals = Registrar::withFeature('approve_new');
+ foreach ($approvals as $type => $config) {
+ $table = $wpdb->prefix . BASE . 'approval_' . $type . 'requests';
+ $votes = $wpdb->prefix . BASE . 'approval_' . $type . 'votes';
- default:
- $error = 'Invalid action';
+ $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 (!empty($data)) {
- JVB()->queue()->queueOperation(
- 'notification_'.$action,
- $args['user_id'],
- $data
- );
- return new WP_REST_Response([
- 'success' => true,
- 'message' => __('Notification queued for processing', 'jvb')
- ]);
- } else {
- return new WP_REST_Response([
- 'success' => false,
- 'message' => __('Error: '.$error, 'jvb')
- ]);
- }
- }
+ if (is_array($requested_by) && in_array($user_id, $requested_by)) {
+ continue;
+ }
- /**
- * Get notification data by ID
- * @param int $notification_id Notification ID
- * @return array|false Notification data or false if not found
- */
- protected function getNotification(int $notification_id):array|false
- {
- global $wpdb;
- $table = $wpdb->prefix.BASE.'notifications';
- $notification = $wpdb->get_row(
- $wpdb->prepare(
- "SELECT * FROM {$table} WHERE id = %d",
- $notification_id
- ),
- ARRAY_A
- );
+ $formatted[] = $this->formatApprovalNotification($approval);
+ }
+ }
- if (!$notification) {
- return false;
- }
+ return $formatted;
+ });
+ }
- // Parse context data
- $context = !empty($notification['context'])
- ? json_decode($notification['context'], true)
- : [];
+ /**
+ * Get content notifications for a user
+ */
+ public function getContentNotifications(
+ int $user_id,
+ string $status = 'unread',
+ int $limit = 20,
+ int $offset = 0
+ ): array {
+ if (!$this->checkUser($user_id)) {
+ return [];
+ }
- return array_merge($notification, $context);
- }
+ $cacheKey = compact('user_id', 'status', 'limit', 'offset');
- /**
- * @param WP_Error|array $result
- * @param object $operation
- * @param array $data
- *
- * @return int|mixed
- */
- public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array
- {
- switch ($operation->type) {
- case 'notification_mark_as_read':
- $result = $this->markRead($data['notification_id'], $operation->user_id);
- break;
- case 'notification_mark_all_as_read':
- $result = $this->markAllRead($data);
- break;
- case 'notification_dismiss_notification':
- $result = $this->markDismissed($data['notification_id'], $operation->user_id);
- break;
- }
- return $result;
- }
+ return $this->cache->remember($cacheKey, function() use ($user_id, $status, $limit, $offset) {
+ global $wpdb;
- /**
- * Mark all notifications as read for a user
- *
- * @param array $data Data containing user_id and optional filters
- *
- * @return array Number of notifications marked as read
- */
- public function markAllRead(array $data):array
- {
- $user_id = $data['user_id'];
- $type = $data['type'] ?? null;
- $notification_ids = $data['notification_ids'] ?? null;
+ // Build status condition
+ $statusCondition = "1=1";
+ if ($status === 'unread') {
+ $statusCondition = "seen.status = 'unread'";
+ } elseif ($status === 'read') {
+ $statusCondition = "seen.status = 'read'";
+ } elseif ($status !== 'all') {
+ $statusCondition = $wpdb->prepare("seen.status = %s", $status);
+ }
- global $wpdb;
- $table = $wpdb->prefix . BASE . 'notifications';
+ $notifications = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT seen.*, content.*
+ FROM {$wpdb->prefix}" . BASE . "notifications_user_seen AS seen
+ JOIN {$wpdb->prefix}" . BASE . "notifications_content AS content
+ ON seen.content_notification_id = content.id
+ WHERE seen.user_id = %d AND {$statusCondition}
+ ORDER BY content.date DESC, content.created_at DESC
+ LIMIT %d OFFSET %d",
+ $user_id,
+ $limit,
+ $offset
+ )
+ );
- $where = [
- 'owner_id = %d',
- "status = 'unread'"
- ];
- $params = [$user_id];
+ // Format content notifications
+ return array_map([$this, 'formatContentNotification'], $notifications);
+ });
+ }
- // Add type filter if specified
- if ($type) {
- $where[] = "type = %s";
- $params[] = $type;
- }
+ // =========================================================================
+ // UPDATE OPERATIONS
+ // =========================================================================
- // Add specific IDs filter if provided
- if ($notification_ids && is_array($notification_ids)) {
- $id_placeholders = implode(',', array_fill(0, count($notification_ids), '%d'));
- $where[] = "id IN ($id_placeholders)";
- $params = array_merge($params, $notification_ids);
- }
+ /**
+ * Mark notification as read
+ */
+ public function markRead(WP_REST_Request $request): WP_REST_Response
+ {
+ $user_id = get_current_user_id();
+ if (!$user_id) {
+ return $this->unauthorized();
+ }
+ $notification_id = absint($request->get_param('notification_id'));
- $where_clause = implode(' AND ', $where);
+ try {
+ $result = $this->notifications->transaction(function($table) use ($notification_id, $user_id) {
+ // Verify ownership
+ $notification = $table
+ ->where(['id' => $notification_id, 'owner_id' => $user_id])
+ ->first();
- // Update all matching notifications
- $updated = $wpdb->query($wpdb->prepare(
- "UPDATE $table
- SET status = 'read',
- read_at = %s,
- updated_at = %s
- WHERE $where_clause",
- array_merge(
- [current_time('mysql'), current_time('mysql')],
- $params
- )
- ));
+ if (!$notification) {
+ throw new Exception('Invalid notification');
+ }
- if ($updated) {
- $this->trackNotificationMetrics(0, $user_id, 'batch_read', ['count' => $updated]);
- $this->clearNotificationCache($user_id);
- }
+ $updated = $table->update(
+ [
+ 'status' => 'read',
+ 'read_at' => current_time('mysql')
+ ],
+ ['id' => $notification_id]
+ );
- return [
- 'success' => true,
- 'result' => $updated
- ];
- }
+ if (!$updated) {
+ throw new Exception('Failed to update notification');
+ }
- /**
- * Mark a notification as read
- *
- * @param int $notification_id Notification ID
- * @param int $user_id User ID making the request (for security)
- *
- * @return array Success or failure
- */
- public function markRead(int $notification_id, int $user_id):array
- {
- if (!$this->checkUser($user_id)) {
- return [
- 'success' => false,
- 'result' => 'Invalid user'
- ];
- }
- global $wpdb;
+ return $updated;
+ });
+
+ $this->trackMetrics($notification_id, $user_id, 'read');
+ $this->clearUserCache($user_id);
+
+ return $this->success(['updated' => $result]);
+
+ } catch (Exception $e) {
+ return $this->error($e->getMessage());
+ }
+ }
+
+ /**
+ * Mark all notifications as read
+ */
+ public function markAllRead(WP_REST_Request $request): WP_REST_Response
+ {
+ $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'];
+ if ($type) $where['type'] = $type;
+
+ $updated = $this->notifications
+ ->where($where)
+ ->updateResults([
+ 'status' => 'read',
+ 'read_at' => current_time('mysql')
+ ]);
+
+ if ($updated) {
+ $this->trackMetrics(0, $user_id, 'batch_read', ['count' => $updated]);
+ $this->clearUserCache($user_id);
+ }
+
+ return $this->success(['updated' => $updated]);
+
+ } catch (Exception $e) {
+ return $this->error($e->getMessage());
+ }
+ }
+
+ /**
+ * Mark notification as actioned
+ */
+ public function markActioned(WP_REST_Request $request): WP_REST_Response
+ {
+ $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) {
+ // Verify ownership and requires action
+ $notification = $table
+ ->where(['id' => $notification_id, 'owner_id' => $user_id, 'requires_action' => 1])
+ ->first();
+
+ if (!$notification) {
+ throw new Exception('Invalid notification or does not require action');
+ }
+
+ $updated = $table->update(
+ [
+ 'status' => 'actioned',
+ 'action_taken' => 1,
+ 'actioned_at' => current_time('mysql')
+ ],
+ ['id' => $notification_id]
+ );
+
+ if (!$updated) {
+ throw new Exception('Failed to update notification');
+ }
+
+ return $updated;
+ });
+
+ $this->trackMetrics($notification_id, $user_id, 'actioned');
+ $this->clearUserCache($user_id);
+
+ return $this->success(['message' => 'Notification actioned']);
+
+ } catch (Exception $e) {
+ return $this->error($e->getMessage());
+ }
+ }
+
+ /**
+ * Dismiss notification
+ */
+ public function markDismissed(WP_REST_Request $request): WP_REST_Response
+ {
+ $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) {
+ // Verify ownership
+ $notification = $table
+ ->where(['id' => $notification_id, 'owner_id' => $user_id])
+ ->first();
+
+ if (!$notification) {
+ throw new Exception('Invalid notification');
+ }
+
+ $updated = $table->update(
+ [
+ 'status' => 'dismissed',
+ 'dismissed_at' => current_time('mysql')
+ ],
+ ['id' => $notification_id]
+ );
+
+ if (!$updated) {
+ throw new Exception('Failed to update notification');
+ }
+
+ return $updated;
+ });
+
+ $this->trackMetrics($notification_id, $user_id, 'dismissed');
+ $this->clearUserCache($user_id);
+
+ return $this->success(['updated' => $result]);
+
+ } catch (Exception $e) {
+ return $this->error($e->getMessage());
+ }
+ }
+
+ /**
+ * Mark content notification as read
+ */
+ public function markContentNotificationRead(int $seen_id, int $user_id): array
+ {
+ if (!$this->checkUser($seen_id) || !$this->checkUser($user_id)) {
+ return [
+ 'success' => false,
+ 'message' => 'Invalid User ID'
+ ];
+ }
+
+ try {
+ global $wpdb;
+ $table = $wpdb->prefix . BASE . 'notifications_user_seen';
+
+ return $wpdb->query('START TRANSACTION') &&
+ $this->updateContentNotificationStatus($table, $seen_id, $user_id, 'read');
+
+ } catch (Exception $e) {
+ global $wpdb;
+ $wpdb->query('ROLLBACK');
+ $this->logError('markContentNotificationRead exception', [
+ 'seen_id' => $seen_id,
+ 'user_id' => $user_id,
+ 'error' => $e->getMessage()
+ ]);
+ return [
+ 'success' => false,
+ 'message' => $e->getMessage()
+ ];
+ }
+ }
+
+ /**
+ * Dismiss content notification
+ */
+ public function dismissContentNotification(int $seen_id, int $user_id): array
+ {
+ if (!$this->checkUser($seen_id) || !$this->checkUser($user_id)) {
+ return [
+ 'success' => false,
+ 'message' => 'Invalid User ID'
+ ];
+ }
+
+ try {
+ global $wpdb;
+ $table = $wpdb->prefix . BASE . 'notifications_user_seen';
+
+ // Verify ownership
+ $seen_record = $wpdb->get_row(
+ $wpdb->prepare(
+ "SELECT * FROM {$table} WHERE id = %d AND user_id = %d",
+ $seen_id,
+ $user_id
+ )
+ );
+
+ if (!$seen_record) {
+ return [
+ 'success' => false,
+ 'message' => 'No notification found'
+ ];
+ }
+
+ $result = $wpdb->update(
+ $table,
+ ['status' => 'dismissed'],
+ ['id' => $seen_id]
+ );
+
+ if ($result !== false) {
+ $this->clearUserCache($user_id);
+ }
+
+ return [
+ 'success' => $result !== false,
+ 'message' => 'Operation completed',
+ ];
+
+ } catch (Exception $e) {
+ return [
+ 'success' => false,
+ 'message' => $e->getMessage()
+ ];
+ }
+ }
+
+ // =========================================================================
+ // QUEUE OPERATIONS
+ // =========================================================================
+
+ /**
+ * Process queued notification operations
+ */
+ public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array
+ {
+ switch ($operation->type) {
+ case 'notification_mark_as_read':
+ $result = $this->markReadQueued($data['notification_id'], $operation->user_id);
+ break;
+ case 'notification_mark_all_as_read':
+ $result = $this->markAllReadQueued($data);
+ break;
+ case 'notification_dismiss_notification':
+ $result = $this->markDismissedQueued($data['notification_id'], $operation->user_id);
+ break;
+ }
+ return $result;
+ }
+
+ /**
+ * Update notification operations (legacy endpoint)
+ */
+ public function updateNotifications(WP_REST_Request $request): WP_REST_Response
+ {
+ $data = $request->get_params();
+ $action = $request->get_param('action');
+ $notificationID = absint($data['notification'] ?? 0);
+ $args = $this->buildParams($request);
+ $queueData = [];
+ $error = '';
+
+ switch ($action) {
+ case 'mark_as_read':
+ if (!$notificationID) {
+ return $this->validationError(['notification' => 'Notification ID is required']);
+ }
+ $queueData = [
+ 'user_id' => $args['user_id'],
+ 'notification_id' => $notificationID,
+ ];
+ break;
+
+ case 'mark_all_as_read':
+ $queueData = ['user_id' => $args['user_id']];
+ if ($request->get_param('notification_ids')) {
+ $queueData['notification_ids'] = array_map('intval', $request->get_param('notification_ids'));
+ }
+ if ($request->get_param('type')) {
+ $queueData['type'] = $request->get_param('type');
+ }
+ break;
+
+ case 'dismiss_notification':
+ $queueData = [
+ 'user_id' => $args['user_id'],
+ 'notification_id' => $notificationID,
+ ];
+ break;
+
+ default:
+ $error = 'Invalid action';
+ }
+
+ if (!empty($queueData)) {
+ Queue::getInstance()->queueOperation(
+ 'notification_' . $action,
+ $args['user_id'],
+ $queueData
+ );
+ return $this->success(['message' => __('Notification queued for processing', 'jvb')]);
+ }
+
+ return $this->error($error ?: 'Unknown error');
+ }
+
+ // =========================================================================
+ // FORMATTING HELPERS
+ // =========================================================================
+
+ /**
+ * Format a notification for display
+ */
+ protected function formatNotification(object $notification): array
+ {
+ $config = $this->notification_types[$notification->type] ?? [];
+ $context = json_decode($notification->context ?? '{}', true);
+
+ // Get action user's name if available
+ $acting_user_name = null;
+ if ($notification->action_user_id) {
+ $acting_user_name = jvbShareName($notification->action_user_id);
+ }
- //TODO: We need to set up a system to check the main notification table, but also the content notification table
- // Verify ownership
- $table = $wpdb->prefix . BASE . 'notifications';
- $notification = $wpdb->get_row(
- $wpdb->prepare(
- "SELECT * FROM {$table}
- WHERE id = %d AND owner_id = %d",
- $notification_id,
- $user_id
- )
- );
+ return [
+ 'id' => $notification->id,
+ 'type' => $notification->type,
+ 'message' => $notification->message,
+ 'created_at' => $notification->created_at,
+ 'status' => $notification->status,
+ 'requires_action' => (bool)$notification->requires_action,
+ 'action_taken' => (bool)$notification->action_taken,
+ 'icon' => $config['icon'] ?? 'info',
+ 'priority' => $notification->priority,
+ 'target' => [
+ 'id' => $notification->target_id,
+ 'type' => $notification->target_type
+ ],
+ 'context' => $context,
+ 'acting_user' => $notification->action_user_id ? [
+ 'id' => $notification->action_user_id,
+ 'name' => $acting_user_name
+ ] : null,
+ 'actions' => $this->getNotificationActions($notification->type, (array)$notification, $notification)
+ ];
+ }
- if (!$notification) {
- return [
- 'success' => false,
- 'result' => 'Invalid notification'
- ];
- }
+ /**
+ * Format an approval request as a notification
+ */
+ protected function formatApprovalNotification(object $approval): array
+ {
+ $data = json_decode($approval->data ?? '{}', true);
+ $type_labels = [
+ 'artist_approval' => 'Artist Verification',
+ 'term_suggestion' => 'Term Suggestion'
+ ];
- // Mark as read
- $result = $wpdb->update(
- $wpdb->prefix . BASE . 'notifications',
- [
- 'status' => 'read',
- 'read_at' => current_time('mysql'),
- 'updated_at' => current_time('mysql')
- ],
- ['id' => $notification_id]
- );
+ $status_labels = [
+ 'pending' => 'Pending',
+ 'approved' => 'Approved',
+ 'rejected' => 'Rejected',
+ 'expired' => 'Expired'
+ ];
- if ($result) {
- $this->clearNotificationCache($user_id);
- }
+ $icon = ($approval->type === 'artist_approval') ? 'artist' : 'style';
- return [
- 'success' => $result !== false,
- 'result' => $result
- ];
- }
+ $message = '';
+ if ($approval->type === 'artist_approval') {
+ if ($approval->requested_by == $approval->target_id) {
+ $message = "Your artist verification is {$status_labels[$approval->status]}";
+ } else {
+ $name = $data['display_name'] ?? 'An artist';
+ $message = "{$name} is requesting verification";
+ }
+ } elseif ($approval->type === 'term_suggestion') {
+ $term_name = $data['term_name'] ?? 'A term';
+ $taxonomy = $data['taxonomy'] ?? '';
+ $taxonomy_name = str_replace(BASE, '', $taxonomy);
- /**
- * Mark a notification as actioned
- *
- * @param int $notification_id Notification ID
- * @param int $user_id User ID making the request
- *
- * @return array
- */
- public function markActioned(int $notification_id, int $user_id):array
- {
- if (!$this->checkUser($user_id)) {
- return [
- 'success' => false,
- 'message' => 'Invalid user'
- ];
- }
- global $wpdb;
+ if ($approval->requested_by == get_current_user_id()) {
+ $message = "Your {$taxonomy_name} suggestion '{$term_name}' is {$status_labels[$approval->status]}";
+ } else {
+ $message = "New {$taxonomy_name} suggestion: '{$term_name}'";
+ }
+ }
- //TODO: We need to set up a system to check the main notification table, but also the content notification table
- // Start a transaction
- $wpdb->query('START TRANSACTION');
+ return [
+ 'id' => 'approval_' . $approval->id,
+ 'type' => $approval->type,
+ 'message' => $message,
+ 'created_at' => $approval->created_at,
+ 'status' => $approval->status,
+ 'requires_action' => ($approval->status === 'pending' && $approval->requested_by != get_current_user_id()),
+ 'action_taken' => !empty($approval->user_vote) && $approval->user_vote !== 'none',
+ 'icon' => $icon,
+ 'priority' => 'high',
+ 'target' => [
+ 'id' => $approval->target_id,
+ 'type' => $approval->target_type
+ ],
+ 'context' => $data,
+ 'approval_data' => [
+ 'required_approvals' => $approval->required_approvals,
+ 'current_approvals' => $approval->current_approvals,
+ 'expires_at' => $approval->expires_at
+ ]
+ ];
+ }
- try {
- $table = $wpdb->prefix.BASE.'notifications';
- // Verify ownership and that notification requires action
- $notification = $wpdb->get_row(
- $wpdb->prepare(
- "SELECT * FROM {$table}
- WHERE id = %d AND user_id = %d AND requires_action = 1
- FOR UPDATE", // Lock the row
- $notification_id,
- $user_id
- )
- );
+ /**
+ * Format a content notification for display
+ */
+ protected function formatContentNotification(object $notification): array
+ {
+ // Get artist data
+ $artist_data = jvbContentFromUser($notification->user_id);
- if (!$notification) {
- $wpdb->query('ROLLBACK');
- $this->logError("Failed to action notification - not found or not owned", [
- 'notification_id' => $notification_id,
- 'user_id' => $user_id
- ], 'warning');
- return [
- 'success' => false,
- 'message' => 'No notification found, or invalid owner'
- ];
- }
+ // Parse JSON data
+ $new_items = json_decode($notification->new_items, true) ?: [];
+ $updated_items = json_decode($notification->updated_items, true) ?: [];
- // Mark as actioned
- $result = $wpdb->update(
- $wpdb->prefix . BASE . 'notifications',
- [
- 'status' => 'actioned',
- 'action_taken' => 1,
- 'actioned_at' => current_time('mysql'),
- 'updated_at' => current_time('mysql')
- ],
- ['id' => $notification_id]
- );
+ // Count items by type
+ $counts_by_type = [];
+ $total_new = 0;
- if ($result !== false) {
- $wpdb->query('COMMIT');
- $this->clearNotificationCache($user_id);
- return [
- 'success' => true,
- 'message' => 'Notification actioned'
- ];
- } else {
- $wpdb->query('ROLLBACK');
- $this->logError("Database error marking notification as actioned", [
- 'notification_id' => $notification_id,
- 'user_id' => $user_id,
- 'db_error' => $wpdb->last_error
- ]);
- return [
- 'success' => false,
- 'message' => 'Error'
- ];
- }
- } catch (Exception $e) {
- $wpdb->query('ROLLBACK');
- $this->logError("Exception marking notification as actioned", [
- 'notification_id' => $notification_id,
- 'user_id' => $user_id,
- 'error' => $e->getMessage()
- ]);
- return [
- 'success' => false,
- 'message' => $e->getMessage(),
- ];
- }
- }
+ foreach ($new_items as $type => $ids) {
+ $clean_type = str_replace(BASE, '', $type);
+ $counts_by_type[$clean_type] = [
+ 'new' => count($ids),
+ 'updated' => 0
+ ];
+ $total_new += count($ids);
+ }
- /**
- * Dismiss a notification
- *
- * @param int $notification_id Notification ID
- * @param int $user_id User ID making the request
- *
- * @return array Success or failure
- */
- public function markDismissed(int $notification_id, int $user_id):array{
- if (!$this->checkUser($user_id)) {
- return [
- 'success' => false,
- 'result' => 'Invalid User',
- ];
- }
- global $wpdb;
+ foreach ($updated_items as $type => $ids) {
+ $clean_type = str_replace(BASE, '', $type);
+ if (!isset($counts_by_type[$clean_type])) {
+ $counts_by_type[$clean_type] = ['new' => 0];
+ }
+ $counts_by_type[$clean_type]['updated'] = count($ids);
+ }
- // Verify ownership
- $table = $wpdb->prefix.BASE.'notifications';
- $notification = $wpdb->get_row(
- $wpdb->prepare(
- "SELECT * FROM {$table}
- WHERE id = %d AND user_id = %d",
- $notification_id,
- $user_id
- )
- );
-//TODO: We need to set up a system to check the main notification table, but also the content notification table
- if (!$notification) {
- return [
- 'success' => false,
- 'result' => 'Invalid notification'
- ];
- }
+ // Build summary text
+ $summary = [];
+ foreach ($counts_by_type as $type => $counts) {
+ if ($counts['new'] > 0) {
+ $label = $counts['new'] === 1 ? $type : $this->pluralize($type);
+ $summary[] = "{$counts['new']} new {$label}";
+ }
+ if ($counts['updated'] > 0) {
+ $label = $counts['updated'] === 1 ? $type : $this->pluralize($type);
+ $summary[] = "{$counts['updated']} updated {$label}";
+ }
+ }
- // Mark as dismissed
- $result = $wpdb->update(
- $wpdb->prefix . BASE . 'notifications',
- [
- 'status' => 'dismissed',
- 'updated_at' => current_time('mysql')
- ],
- [ 'id' => $notification_id ]
- );
+ return [
+ 'id' => $notification->id,
+ 'seen_id' => $notification->content_notification_id,
+ 'status' => $notification->status,
+ 'date' => $notification->date,
+ 'artist' => $artist_data,
+ 'new_items' => $new_items,
+ 'updated_items' => $updated_items,
+ 'summary' => implode(', ', $summary),
+ 'total_new' => $total_new,
+ 'total_updated' => $notification->total_items - $total_new,
+ 'counts_by_type' => $counts_by_type,
+ 'has_profile_update' => (bool)$notification->has_profile_update,
+ 'created_at' => $notification->created_at
+ ];
+ }
- if ($result) {
- $this->clearNotificationCache($user_id);
- }
+ /**
+ * Build a message for grouped notifications
+ */
+ protected function buildGroupedMessage(string $user_name, string $type, int $count, array $target_types = []): string
+ {
+ switch ($type) {
+ case 'new_favourite':
+ if (count($target_types) === 1) {
+ $content_type = $this->manager->getContentTypeLabel($target_types[0]);
+ return "{$user_name} favourited {$count} of your {$content_type}";
+ }
+ return "{$user_name} favourited {$count} of your items";
- return [
- 'success' => $result !== false,
- 'result' => $result
- ];
- }
+ case 'artist_request':
+ return "{$user_name} wants to join your shop";
- /***
- * Content notifications (in a separate database))
- */
- /**
- * Get content notifications for a user
- *
- * @param int $user_id User ID
- * @param string $status Status filter (unread, read, all)
- * @param int $limit Maximum number of notifications to return
- * @param int $offset Pagination offset
- *
- * @return array Content notifications
- */
- public function getContentNotifications(
- int $user_id,
- string $status = 'unread',
- int $limit = 20,
- int $offset = 0
- ):array {
- if (!$this->checkUser($user_id)) {
- return [];
- }
- // Try cache first
- $cache_key = "user_{$user_id}_content_notifications_{$status}_{$limit}_{$offset}";
- $cached = $this->cache->get($cache_key);
+ default:
+ return "{$user_name} has {$count} notifications for you";
+ }
+ }
- if ($cached !== false) {
- return $cached;
- }
+ // =========================================================================
+ // HELPER METHODS
+ // =========================================================================
- global $wpdb;
+ /**
+ * Get notification actions
+ */
+ protected function getNotificationActions(string $type, array $data, ?object $notification = null): array
+ {
+ $actions = [];
- // Build status condition
- $status_condition = "1=1";
- if ($status === 'unread') {
- $status_condition = "seen.status = 'unread'";
- } elseif ($status === 'read') {
- $status_condition = "seen.status = 'read'";
- } elseif ($status !== 'all') {
- $status_condition = $wpdb->prepare("seen.status = %s", $status);
- }
+ switch ($type) {
+ case 'artist_approved':
+ case 'artist_rejected':
+ case 'shop_approved':
+ case 'shop_rejected':
+ case 'term_approved':
+ case 'term_rejected':
+ case 'system_message':
+ // No extra action needed
+ break;
- // Get content notifications this user has seen records for
+ case 'artist_invitation':
+ $actions[] = [
+ 'icon' => 'upvote',
+ 'label' => 'Approve',
+ 'action' => 'acceptInvitation',
+ ];
+ $actions[] = [
+ 'icon' => 'downvote',
+ 'label' => 'Reject',
+ 'action' => 'reject_invitation',
+ ];
+ break;
- $notifications = $wpdb->get_results(
- $wpdb->prepare(
- "SELECT seen.*, content.*
- FROM {$wpdb->prefix}{$this->base}notifications_user_seen AS seen
- JOIN {$wpdb->prefix}{$this->base}notifications_content AS content
- ON seen.content_notification_id = content.id
- WHERE seen.user_id = %d AND {$status_condition}
- ORDER BY content.date DESC, content.created_at DESC
- LIMIT %d OFFSET %d",
- $user_id,
- $limit,
- $offset
- )
- );
+ case 'artist_request':
+ $actions[] = [
+ 'icon' => 'upvote',
+ 'label' => 'Approve',
+ 'action' => 'accept_to_shop',
+ ];
+ $actions[] = [
+ 'icon' => 'downvote',
+ 'label' => 'Reject',
+ 'action' => 'reject_to_shop',
+ ];
+ break;
- // Format content notifications
- $formatted = [];
- foreach ($notifications as $notification) {
- $formatted[] = $this->formatContentNotification($notification);
- }
+ case 'new_term':
+ $actions[] = [
+ 'icon' => 'upvote',
+ 'label' => 'Approve',
+ 'action' => 'approve_term',
+ ];
+ $actions[] = [
+ 'icon' => 'downvote',
+ 'label' => 'Reject',
+ 'action' => 'reject_term',
+ ];
+ break;
- // Cache the results
- $this->cache->set($cache_key, $formatted, 'notifications_' . $user_id);
+ case 'list_shared':
+ if (!empty($data['list_id'])) {
+ $actions[] = [
+ 'icon' => 'list-heart',
+ 'label' => 'View List',
+ 'url' => home_url("/dash/favourites/{$data['list_id']}"),
+ ];
+ }
+ break;
- return $formatted;
- }
+ default:
+ if (!empty($data['target_id']) && !empty($data['target_type'])) {
+ $actions[] = [
+ 'icon' => 'link',
+ 'label' => 'View',
+ 'url' => $this->getItemLink($data['target_id'], $data['target_type']),
+ ];
+ }
+ break;
+ }
- /**
- * Format a content notification for display
- *
- * @param object $notification Content notification object
- *
- * @return array Formatted content notification
- */
- protected function formatContentNotification(object $notification):array
- {
- // Get artist data
- $artist_data = jvbContentFromUser($notification->user_id);
+ $actions[] = [
+ 'icon' => 'close',
+ 'label' => 'Dismiss',
+ 'action' => 'dismiss_notification'
+ ];
- // Parse JSON data
- $new_items = json_decode($notification->new_items, true) ?: [];
- $updated_items = json_decode($notification->updated_items, true) ?: [];
+ return apply_filters('jvb_notification_actions', $actions, $type, $data, $notification);
+ }
- // Count items by type
- $counts_by_type = [];
- $total_new = 0;
+ /**
+ * Get item link for notification target
+ */
+ 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);
+ }
+ }
- foreach ($new_items as $type => $ids) {
- $clean_type = str_replace(BASE, '', $type);
- $counts_by_type[ $clean_type ] = [
- 'new' => count($ids),
- 'updated' => 0
- ];
- $total_new += count($ids);
- }
+ /**
+ * Get notification data by ID
+ */
+ protected function getNotification(int $notification_id): array|false
+ {
+ $notification = $this->notifications->where(['id' => $notification_id])->first();
- foreach ($updated_items as $type => $ids) {
- $clean_type = str_replace(BASE, '', $type);
- if (!isset($counts_by_type[ $clean_type ])) {
- $counts_by_type[ $clean_type ] = [
- 'new' => 0
- ];
- }
- $counts_by_type[ $clean_type ]['updated'] = count($ids);
- }
+ if (!$notification) {
+ return false;
+ }
- // Build summary text
- $summary = [];
- foreach ($counts_by_type as $type => $counts) {
- if ($counts['new'] > 0) {
- $label = $counts['new'] === 1 ? $type : $this->pluralize($type);
- $summary[] = "{$counts['new']} new {$label}";
- }
- if ($counts['updated'] > 0) {
- $label = $counts['updated'] === 1 ? $type : $this->pluralize($type);
- $summary[] = "{$counts['updated']} updated {$label}";
- }
- }
+ $context = !empty($notification->context)
+ ? json_decode($notification->context, true)
+ : [];
- return [
- 'id' => $notification->id,
- 'seen_id' => $notification->content_notification_id,
- 'status' => $notification->status,
- 'date' => $notification->date,
- 'artist' => $artist_data,
- 'new_items' => $new_items,
- 'updated_items' => $updated_items,
- 'summary' => implode(', ', $summary),
- 'total_new' => $total_new,
- 'total_updated' => $notification->total_items - $total_new,
- 'counts_by_type' => $counts_by_type,
- 'has_profile_update' => (bool) $notification->has_profile_update,
- 'created_at' => $notification->created_at
- ];
- }
+ return array_merge((array)$notification, $context);
+ }
- /**
- * Mark a content notification as read
- *
- * @param int $seen_id User seen record ID
- * @param int $user_id User ID making the request
- *
- * @return array Success or failure
- */
- public function markContentNotificationRead(int $seen_id, int $user_id):array
- {
- // Validate input parameters
- if (!$this->checkUser($seen_id)) {
- $this->logError("Invalid seen ID", [
- 'seen_id' => $seen_id,
- 'user_id' => $user_id
- ], 'warning');
- return [
- 'success' => false,
- 'message' => 'Invalid User ID'
- ];
- }
+ /**
+ * Build notification request parameters
+ */
+ protected function buildParams(WP_REST_Request $request): array
+ {
+ $params = $request->get_params();
- if (!$this->checkUser($user_id)) {
- $this->logError("Invalid user ID", [
- 'seen_id' => $seen_id,
- 'user_id' => $user_id
- ], 'warning');
- return [
- 'success' => false,
- 'message' => 'Invalid User ID'
- ];
- }
+ return [
+ 'status' => in_array($params['status'] ?? '', ['all', 'unread', 'expired'])
+ ? $params['status']
+ : 'unread',
+ 'user_id' => get_current_user_id(),
+ 'page' => absint($params['page'] ?? 1),
+ 'type' => in_array($params['type'] ?? '', array_keys($this->manager->notification_types ?? []))
+ ? $params['type']
+ : 'all',
+ ];
+ }
- global $wpdb;
- $table = $wpdb->prefix . BASE . 'notifications_user_seen';
+ /**
+ * Determine which table a notification type belongs to
+ */
+ protected function getTableForNotificationType(string $notificationType): string
+ {
+ foreach ($this->notificationTableMap as $table => $types) {
+ if (in_array($notificationType, $types)) {
+ return $table;
+ }
+ }
+ return 'notifications';
+ }
- // Start transaction
- $wpdb->query('START TRANSACTION');
+ /**
+ * Simple pluralization helper
+ */
+ protected function pluralize(string $word): string
+ {
+ $irregular = [
+ 'tattoo' => 'tattoos',
+ 'piercing' => 'piercings',
+ 'artwork' => 'artwork',
+ 'news' => 'news',
+ 'offer' => 'offers',
+ 'event' => 'events'
+ ];
- try {
- // Verify ownership with row locking
- $seen_record = $wpdb->get_row(
- $wpdb->prepare(
- "SELECT * FROM {$table}
- WHERE id = %d AND user_id = %d
- FOR UPDATE",
- $seen_id,
- $user_id
- )
- );
+ if (isset($irregular[$word])) {
+ return $irregular[$word];
+ }
- if (!$seen_record) {
- $wpdb->query('ROLLBACK');
- $this->logError("Seen record not found or not owned by user", [
- 'seen_id' => $seen_id,
- 'user_id' => $user_id
- ], 'warning');
- return [
- 'success' => false,
- 'message' => 'Seen record not found or not owned by user'
- ];
- }
+ if (substr($word, -1) === 'y') {
+ return substr($word, 0, -1) . 'ies';
+ }
- // Mark as read
- $result = $wpdb->update(
- $table,
- [
- 'status' => 'read',
- 'read_at' => current_time('mysql')
- ],
- ['id' => $seen_id]
- );
+ return $word . 's';
+ }
- if ($result !== false) {
- $wpdb->query('COMMIT');
- $this->clearNotificationCache($user_id);
- return [
- 'success' => true,
- 'message' => 'Successfully marked as seen'
- ];
- } else {
- $wpdb->query('ROLLBACK');
- $this->logError("Database error marking content notification as read", [
- 'seen_id' => $seen_id,
- 'user_id' => $user_id,
- 'db_error' => $wpdb->last_error
- ]);
- return [
- 'success' => false,
- 'message' => 'Something went wrong...'
- ];
- }
- } catch (Exception $e) {
- $wpdb->query('ROLLBACK');
- $this->logError("Exception marking content notification as read", [
- 'seen_id' => $seen_id,
- 'user_id' => $user_id,
- 'error' => $e->getMessage()
- ]);
- return [
- 'success' => false,
- 'message' => $e->getMessage()
- ];
- }
- }
+ /**
+ * Track notification metrics
+ */
+ protected function trackMetrics(int $notification_id, int $user_id, string $action, array $details = []): void
+ {
+ try {
+ $this->metrics->create([
+ 'notification_id' => $notification_id,
+ 'user_id' => $user_id,
+ 'action' => $action,
+ 'action_source' => 'web',
+ 'action_details' => !empty($details) ? json_encode($details) : null,
+ ]);
+ } catch (Exception $e) {
+ // Don't fail the request if metrics tracking fails
+ $this->logError('trackMetrics failed', [
+ 'error' => $e->getMessage(),
+ 'user_id' => $user_id,
+ 'action' => $action
+ ]);
+ }
+ }
+ /**
+ * Clear notification cache for a user
+ */
+ protected function clearUserCache(int $user_id): void
+ {
+ Cache::invalidateItem('notifications', $user_id);
+ }
- /**
- * Dismiss a content notification
- *
- * @param int $seen_id User seen record ID
- * @param int $user_id User ID making the request
- *
- * @return array Success or failure
- */
- public function dismissContentNotification( int $seen_id, int $user_id ):array
- {
- // Validate input parameters
- if (!$this->checkUser($seen_id)) {
- $this->logError("Invalid seen ID", [
- 'seen_id' => $seen_id,
- 'user_id' => $user_id
- ], 'warning');
- return [
- 'success' => false,
- 'message' => 'Invalid User ID'
- ];
- }
+ /**
+ * Update content notification status (helper for transactions)
+ */
+ private function updateContentNotificationStatus(string $table, int $seen_id, int $user_id, string $status): array
+ {
+ global $wpdb;
- if (!$this->checkUser($user_id)) {
- $this->logError("Invalid user ID", [
- 'seen_id' => $seen_id,
- 'user_id' => $user_id
- ], 'warning');
- return [
- 'success' => false,
- 'message' => 'Invalid User ID'
- ];
- }
- global $wpdb;
+ // Verify ownership with row locking
+ $seen_record = $wpdb->get_row(
+ $wpdb->prepare(
+ "SELECT * FROM {$table} WHERE id = %d AND user_id = %d FOR UPDATE",
+ $seen_id,
+ $user_id
+ )
+ );
- // Verify ownership
- $seen_record = $wpdb->get_row(
- $wpdb->prepare(
- "SELECT * FROM {$wpdb->prefix}{$this->base}notifications_user_seen
- WHERE id = %d AND user_id = %d",
- $seen_id,
- $user_id
- )
- );
+ if (!$seen_record) {
+ $wpdb->query('ROLLBACK');
+ return [
+ 'success' => false,
+ 'message' => 'Seen record not found or not owned by user'
+ ];
+ }
- if (!$seen_record) {
- return [
- 'success' => false,
- 'message' => 'No notification found'
- ];
- }
+ // Update status
+ $result = $wpdb->update(
+ $table,
+ [
+ 'status' => $status,
+ 'read_at' => current_time('mysql')
+ ],
+ ['id' => $seen_id]
+ );
- // Mark as dismissed
- $result = $wpdb->update(
- $wpdb->prefix . BASE . 'notifications_user_seen',
- [
- 'status' => 'dismissed'
- ],
- [ 'id' => $seen_id ]
- );
+ if ($result !== false) {
+ $wpdb->query('COMMIT');
+ $this->clearUserCache($user_id);
+ return [
+ 'success' => true,
+ 'message' => 'Successfully marked as ' . $status
+ ];
+ }
- if ($result) {
- $this->clearNotificationCache($user_id);
- }
+ $wpdb->query('ROLLBACK');
+ return [
+ 'success' => false,
+ 'message' => 'Database error'
+ ];
+ }
- return [
- 'success' => $result !== false,
- 'message' => 'Operation completed',
- ];
- }
+ /**
+ * Queued operation helpers (for backwards compatibility)
+ */
+ private function markReadQueued(int $notification_id, int $user_id): array
+ {
+ $updated = $this->notifications->update(
+ ['status' => 'read', 'read_at' => current_time('mysql')],
+ ['id' => $notification_id, 'owner_id' => $user_id]
+ );
- /**
- * Simple pluralization helper
- *
- * @param string $word Word to pluralize
- * @return string Pluralized word
- */
- protected function pluralize(string $word): string
- {
- $irregular = [
- 'tattoo' => 'tattoos',
- 'piercing' => 'piercings',
- 'artwork' => 'artwork',
- 'news' => 'news',
- 'offer' => 'offers',
- 'event' => 'events'
- ];
+ if ($updated) {
+ $this->clearUserCache($user_id);
+ }
- if (isset($irregular[$word])) {
- return $irregular[$word];
- }
+ return ['success' => $updated !== false];
+ }
- // Simple pluralization rules
- if (substr($word, -1) === 'y') {
- return substr($word, 0, -1) . 'ies';
- }
+ private function markAllReadQueued(array $data): array
+ {
+ $where = ['owner_id' => $data['user_id'], 'status' => 'unread'];
+ if (!empty($data['type'])) {
+ $where['type'] = $data['type'];
+ }
- return $word . 's';
- }
- /**
- * Track notification metrics for analytics
- *
- * @param int $notification_id Notification ID (0 for batch operations)
- * @param int $user_id User ID
- * @param string $action Action taken (read, dismiss, etc.)
- * @param array $details Additional details
- * @return bool Success or failure
- */
- protected function trackNotificationMetrics(int $notification_id, int $user_id, string $action, array $details = []): bool
- {
- global $wpdb;
- $metrics_table = $wpdb->prefix . BASE . 'notification_metrics';
+ $updated = $this->notifications
+ ->where($where)
+ ->updateResults(['status' => 'read', 'read_at' => current_time('mysql')]);
- try {
- return $wpdb->insert(
- $metrics_table,
- [
- 'notification_id' => $notification_id,
- 'user_id' => $user_id,
- 'action' => $action,
- 'action_source' => 'web',
- 'action_details' => json_encode($details),
- 'created_at' => current_time('mysql')
- ]
- ) !== false;
- } catch (Exception $e) {
- $this->logError("Failed to track notification metrics", [
- 'user_id' => $user_id,
- 'action' => $action,
- 'error' => $e->getMessage()
- ]);
- return false;
- }
- }
+ if ($updated) {
+ $this->clearUserCache($data['user_id']);
+ }
- /**
- * Clear notification cache for a user, for all notification types
- *
- * @param int $user_id User ID
- * @return void
- */
- protected function clearNotificationCache(int $user_id): void
- {
- // Clear regular notifications cache
- $this->cache->invalidate("user_{$user_id}_notifications_");
- $this->cache->invalidate("user_{$user_id}_regular_notifications_");
+ return ['success' => $updated !== false, 'count' => $updated];
+ }
- // Clear content notifications cache
- $this->cache->invalidate("user_{$user_id}_content_notifications_");
+ private function markDismissedQueued(int $notification_id, int $user_id): array
+ {
+ $updated = $this->notifications->update(
+ ['status' => 'dismissed', 'dismissed_at' => current_time('mysql')],
+ ['id' => $notification_id, 'owner_id' => $user_id]
+ );
- // Clear approval notifications cache
- $this->cache->invalidate("user_{$user_id}_approval_notifications_");
+ if ($updated) {
+ $this->clearUserCache($user_id);
+ }
- // Clear merged notifications cache
- $this->cache->invalidate("user_{$user_id}_merged_notifications_");
- }
+ return ['success' => $updated !== false];
+ }
}
--
Gitblit v1.10.0