| | |
| | | <?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]; |
| | | } |
| | | } |