From 48721c85ebcfa973ee81719d2467ca80e4253dc9 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 01 May 2026 17:30:03 +0000
Subject: [PATCH] =Edmonton Ink hard test begins! Real testing of the managers and reset routes will commence. So far, just ensuring our classes are all loaded correctly: Site() and its sub-classes Membership, Login, etc. Care should be taken to load conditionally on 'init', as we finish defining most settings by 'plugins_loaded' at priority 5
---
inc/rest/routes/NotificationsRoutes.php | 2932 +++++++++++++++++++++++++++--------------------------------
1 files changed, 1,337 insertions(+), 1,595 deletions(-)
diff --git a/inc/rest/routes/NotificationsRoutes.php b/inc/rest/routes/NotificationsRoutes.php
index ea6fa2c..e1ea571 100644
--- a/inc/rest/routes/NotificationsRoutes.php
+++ b/inc/rest/routes/NotificationsRoutes.php
@@ -1,1754 +1,1496 @@
<?php
namespace JVBase\rest\routes;
-use JVBase\JVB;
-use JVBase\rest\RestRouteManager;
+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',
+ 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 = JVB()->notification();
+ $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([
+ 'user' => 'integer|required',
+ '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([
+ 'user' => 'integer|required',
+ 'notification_id' => 'integer|required',
+ ])
+ ->auth('user')
+ ->rateLimit(30)
+ ->register();
+ // Mark all as read
+ Route::for('notifications/read-all')
+ ->post([$this, 'markAllRead'])
+ ->args([
+ 'user' => 'integer|required',
+ '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([
+ 'user' => 'integer|required',
+ '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([
+ 'user' => 'integer|required',
+ 'notification_id' => 'integer|required',
+ ])
+ ->auth('user')
+ ->rateLimit(30)
+ ->register();
- $actions = [];
+ // Get unread count
+ Route::for('notifications/count')
+ ->get([$this, 'getUnreadCount'])
+ ->args([
+ 'user' => 'integer|required',
+ '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 = 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'));
- 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;
- }
-
- $actions[] = [
- 'icon' => 'close',
- 'label' => 'Dismiss',
- 'action' => 'dismiss_notification'
- ];
- // Allow customization via filter
- return apply_filters('jvb_notification_actions', $actions, $type, $data, $notification);
- }
-
- /**
- * @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?'
- ]);
+ if (!$this->checkUser($user_id)) {
+ return $this->unauthorized();
}
- // Check HTTP cache headers (includes notification types in timestamp check)
- $cache_check = $this->checkUserHeaders($request, $user_id, 'notifications');
- if ($cache_check) {
- return $cache_check;
+ $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);
+ }
+
+ /**
+ * Get unread notification count
+ */
+ 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)) {
+ return $this->unauthorized();
}
- // 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'];
+ $cacheKey = compact('user_id', '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) {
- $response = new WP_REST_Response($cached);
- return $this->addCacheHeaders($response);
- }
+ $count = $this->cache->remember($cacheKey, function() use ($user_id, $type) {
+ $where = ['owner_id' => $user_id, 'status' => 'unread'];
+ if ($type) $where['type'] = $type;
- try {
- // Step 2: Get regular notifications
- $regular_notifications = $this->getRegularNotifications($user_id, $params);
+ return $this->notifications->where($where)->countResults();
+ });
- // Step 3: Get content notifications
- $content_notifications = $this->getContentNotifications($user_id, $status, $limit, $offset);
+ return $this->success(['count' => $count]);
+ }
- // Step 4: Get approval notifications
- $approval_notifications = $this->getApprovalNotifications($user_id, $status);
+ /**
+ * 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');
- // Step 5: Merge in order of created date
- $notifications = array_merge(
- $regular_notifications,
- $content_notifications,
- $approval_notifications
- );
+ return $this->cache->remember($cacheKey, function() use ($user_id, $status, $limit, $offset, $type) {
+ $time_window = '24 HOUR';
- 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
- });
+ // Build type filter
+ $typeFilter = '';
+ $typeValues = [];
- // Apply pagination
- $total_count = count($notifications);
- $notifications = array_slice($notifications, 0, $limit);
+ 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;
+ }
+ }
- // 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
- ]
- ];
+ // Build status filter
+ $statusFilter = '';
+ if ($status === 'read') {
+ $statusFilter = "AND status IN ('read', 'actioned')";
+ } elseif ($status !== 'all') {
+ $statusFilter = "AND status = %s";
+ $typeValues[] = $status;
+ }
- // Cache the result
- $this->cache->set($cache_key, $response, 'notifications_' . $user_id);
- $response = new WP_REST_Response($response);
- return $this->addCacheHeaders($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;
- }
-
- $formatted[] = $this->formatApprovalNotification($approval);
- }
- }
- }
-
- // Cache the results
- $this->cache->set($cache_key, $formatted, 'approvals');
-
- return $formatted;
- }
-
- /**
- * 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'
- ];
-
- $status_labels = [
- 'pending' => 'Pending',
- 'approved' => 'Approved',
- 'rejected' => 'Rejected',
- 'expired' => 'Expired'
- ];
-
- $icon = ($approval->type === 'artist_approval') ? 'artist' : 'style';
-
- $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);
-
- 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 [
- '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
- ]
- ];
- }
-
- /**
- * 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
- * @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;
- }
-
- global $wpdb;
-
- // 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);
- }
- }
- try {
- // Time window for grouping (e.g., last 24 hours)
- $time_window = '24 HOUR';
-
- $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::getFeatured('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 = absint($request->get_param('user'));
+ $notification_id = absint($request->get_param('notification_id'));
- $where_clause = implode(' AND ', $where);
+ if (!$this->checkUser($user_id)) {
+ return $this->unauthorized();
+ }
- // 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
- )
- ));
+ 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 ($updated) {
- $this->trackNotificationMetrics(0, $user_id, 'batch_read', ['count' => $updated]);
- $this->clearNotificationCache($user_id);
- }
+ if (!$notification) {
+ throw new Exception('Invalid notification');
+ }
- return [
- 'success' => true,
- 'result' => $updated
- ];
- }
+ $updated = $table->update(
+ [
+ 'status' => 'read',
+ 'read_at' => current_time('mysql')
+ ],
+ ['id' => $notification_id]
+ );
- /**
- * 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;
+ if (!$updated) {
+ throw new Exception('Failed to update notification');
+ }
+
+ 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 = absint($request->get_param('user'));
+ $type = sanitize_text_field($request->get_param('type') ?? '');
+
+ if (!$this->checkUser($user_id)) {
+ return $this->unauthorized();
+ }
+
+ 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 = absint($request->get_param('user'));
+ $notification_id = absint($request->get_param('notification_id'));
+
+ if (!$this->checkUser($user_id)) {
+ return $this->unauthorized();
+ }
+
+ 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 = absint($request->get_param('user'));
+ $notification_id = absint($request->get_param('notification_id'));
+
+ if (!$this->checkUser($user_id)) {
+ return $this->unauthorized();
+ }
+
+ 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)) {
+ JVB()->queue()->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' => absint($params['user'] ?? 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