cache_name = 'shop'; parent::__construct(); add_filter(BASE.'handle_bulk_operation', [$this, 'generateThumbnail'], 10, 3); add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); $this->content_type = 'shop'; $this->type = 'post'; $this->action = 'dash-'; $this->operation_type = 'shop_update'; } /** * Registers Shop routes * @return void */ public function registerRoutes():void { register_rest_route($this->namespace, '/shop', [ [ 'methods' => 'POST', 'callback' => [$this, 'handleShopSettings'], 'permission_callback' => [$this, 'checkOwnerPermission'] ]]); register_rest_route($this->namespace, '/shop/artists', [ [ 'methods' => 'GET', 'callback' => [$this, 'getInvitations'], 'permission_callback' => [$this, 'checkPermission'] ], [ 'methods' => 'POST', 'callback' => [$this, 'handleShopActions'], 'permission_callback' => [$this, 'checkPermission'] ] ]); //For non-shop owners to accept invitations register_rest_route($this->namespace, '/shop/accept', [ [ 'methods' => 'POST', 'callback' => [$this, 'handleShopInvite'], 'permission_callback' => [$this, 'checkPermission'] ] ]); } /** * @param WP_REST_Request $request * * @return WP_REST_Response */ public function handleShopSettings(WP_REST_Request $request):WP_REST_Response { $data = $request->get_params(); $user = $data['user']; if (!$this->checkUser($user) || !$this->userCheck($user)) { return new WP_REST_Response([ 'success' => false, 'message' => 'Looks like you may not be who you say you are...' ]); } if (!$this->checkShop($data['shop'])) { return new WP_REST_Response([ 'success' => false, 'message' => 'This shop doesn\'t exist?' ]); } $queue = JVB()->queue(); unset($data['user']); $operationID = $data['id']; unset($data['id']); $data = json_encode($data); $queue->queueOperation( 'shop_update', $user, $data, [ 'operation_id' => 'u'.$user.'_'.$operationID, 'priority' => 'high', 'notification' => true, ] ); return new WP_REST_Response([ 'success' => true, 'message' => 'Successfully queued for processing' ]); } /** * @param WP_Error|array $result * @param object $operation * @param array $data * * @return WP_Error|array */ public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array { if ($operation->type !== 'shop_update') { return $result; } $user_id = (int)$operation->user_id; $shop = (int)$data['shop']; if (!user_can($user_id, 'manage_shop_'.$shop)) { return [ 'success' => false, 'result' => 'User cannot manage this shop' ]; } $meta = Meta::forTerm($shop); $allowed = Registrar::getFieldsFor('shop'); $setData = array_filter( $data, function ($key) use ($allowed) { return array_key_exists($key, $allowed); }, ARRAY_FILTER_USE_KEY ); error_log('Set data: '.print_r($setData, true)); foreach ($setData as $name => $value) { if ($name === 'shop') { JVB()->routes('shop')->requestShopAdmission($operation->user_id, $value); $setData['requested_shop'] = $value; unset($setData['shop']); } if (array_intersect(array_keys($data), ['image_portrait', 'shop', 'city', 'type', 'top_style','display_name'])) { if (array_key_exists('image_portrait', $data) || $meta->get('image_portrait') !== '') { $this->checkGenerateThumbnail($user_id, $this->buildThumbnailData($user_id)); } } } $meta->setAll($setData); // $results = []; // // foreach ($data as $name => $value) { // $value = $this->getMetaValues($value); // error_log('Value: '.print_r($value, true)); // if ($value === '') { // $results[] = $meta->deleteValue($name); // } else { // if ($name === 'shop') { // JVB()->routes('shop')->requestShopAdmission($operation->user_id, $value); // $results[] = $meta->set('requested_shop', $value); // } // // } // // if (array_intersect(array_keys($data), ['image_portrait', 'shop', 'city', 'type', 'top_style','display_name'])) { // if (array_key_exists('image_portrait', $data) || $meta->get('image_portrait') !== '') { // $this->checkGenerateThumbnail($user_id, $this->buildThumbnailData($user_id)); // } // } // } return [ 'success' => true, 'result' => $result ]; } /** * Queues a featured image generator * @param int $user_id * @param array $data * * @return void */ protected function checkGenerateThumbnail(int $user_id, array $data):void { if (!$this->checkUser($user_id)) { return; } if (empty($data)) { return; } if (!$this->checkShop($data['shop'])) { return; } if (!array_key_exists('name', $data)) { $shop = get_term($data['shop'], BASE.'shop'); $data['name'] = $shop->name; } if (!array_key_exists('city', $data)) { $meta = Meta::forTerm($data['shop']); $city = $meta->get('city'); if ($city !== '') { $city = get_term($city, BASE.'city'); if ($city && !is_wp_error($city)) { $data['city'] = $city->name; } else { return; } } } $queue = JVB()->queue(); $queue->queueOperation( 'shop_image', $user_id, $data ); } /** * Processes the featured image generation operation * @param WP_Error|array $result * @param object $operation * @param array $data * * @return WP_Error|array */ public function generateThumbnail(WP_Error|array $result, object $operation, array $data):WP_Error|array { if ($operation->type !== 'shop_image') { return $result; } $data['imageType'] = 'shop'; $fileGenerator = new UploadManager('shop', $operation->user_id); $generator = new ImageGenerator($data, $fileGenerator); $result = $generator->generate(); if ($result['success']) { $return = $this->setShopFeaturedImage($data['shop'], $result['attachment_id']); if ($return) { return [ 'success' => true, 'result' => 'Image successfully set' ]; } return [ 'success' => false, 'result' => $result ]; } return $result; } public function setShopFeaturedImage(int $shopID, int $imgID):bool { // Check if The SEO Framework is active if (!function_exists('the_seo_framework')) { return false; } // Get the term taxonomy ID $term = get_term($shopID, BASE.'shop'); if (is_wp_error($term)) { return false; } $tt_id = $term->term_taxonomy_id; // Get The SEO Framework instance $tsf = the_seo_framework(); // Get the image URL $image_url = wp_get_attachment_image_url($imgID, 'full'); if (!$image_url) { return false; } // Set both the ID and URL for the term $tsf->update_single_term_meta_item('social_image_id', $imgID, $shopID, $tt_id, BASE.'shop'); $tsf->update_single_term_meta_item('social_image_url', $image_url, $shopID, $tt_id, BASE.'shop'); return true; } /** * @param WP_REST_Request $request * * @return WP_REST_Response */ public function handleShopInvite(WP_REST_Request $request):WP_REST_Response { $data = $request->get_params(); $action = (array_key_exists('action', $data) && in_array($data['action'], ['accept', 'decline', 'request'])) ? $data['action'] : null; if (!$action) { return new WP_REST_Response([ 'success' => false, 'message' => 'Invalid action' ]); } $userID = (int)$request->get_param('user'); $shop = (int)$data['shop']; if (!$this->checkShop($shop) || !$this->checkUser($userID)) { return new WP_REST_Response([ 'success' => false, 'message' => 'Invalid shop or user' ]); } $notification_id = (array_key_exists('notification_id', $data)) ? $data['notification_id'] : false; if (!$notification_id) { return new WP_REST_Response([ 'success' => false, 'message' => 'Invalid notification' ]); } $result = false; switch ($action) { case 'accept': $result = $this->acceptShopInvitation($userID, $data['shop']); break; case 'decline': $result = $this->declineShopInvitation($userID, $data['shop']); break; case 'request': $result = $this->requestShopAdmission($userID, $shop); break; } $start = ($result)? 'Successfully' : 'Unsuccessfully'; return new WP_REST_Response([ 'success' => $result, 'message' => $start.' processed request' ]); } /** * @param WP_REST_Request $request * * @return bool */ public function checkOwnerPermission(WP_REST_Request $request):bool { parent::checkPermission($request); $userID = $request->get_param('user'); if (!$userID && !is_numeric($userID)) { $userID = get_current_user_id(); } $shopID = $request->get_param('shop'); if (!$shopID || !is_numeric($shopID) || !term_exists((int)$shopID, BASE.'shop')) { return false; } return user_can($userID, 'manage_shop_'.$shopID); } /** * @param WP_REST_Request $request * * @return WP_REST_Response */ public function getInvitations(WP_REST_Request $request):WP_REST_Response { $shopID = (int) $request->get_param('shop'); if (!$this->checkShop($shopID)) { return new WP_REST_Response([ 'success' => false, 'message' => 'Invalid shop' ]); } $status = $request->get_param('status') ?? 'all'; $page = $request->get_param('page') ?? 1; return JVB()->routes('artistInvite')->getShopInvitations($shopID, $status, $page); } /** * @param int $userID User ID * @param int $shopID Shop ID * * @return bool */ public function requestShopAdmission(int $userID, int $shopID):bool { if (!$this->checkUser($userID) || !$this->checkShop($shopID)) { return false; } global $wpdb; $table = $wpdb->prefix . BASE . 'artist_shop_requests'; // Check if request already exists $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table WHERE user_id = %d AND shop_id = %d", $userID, $shopID )); if ($existing) { return false; } // Get the artist's post ID $artist_id = get_user_meta($userID, BASE . 'link', true); if (!$artist_id) { return false; } // Insert new request $result = $wpdb->insert( $table, [ 'user_id' => $userID, 'artist_id' => $artist_id, 'shop_id' => $shopID, 'status' => 'requested', 'created_date' => current_time('mysql') ] ); if ($result === false) { return false; } // Notify shop managers/owners about the request $this->notifyShopManagers($shopID, $userID, $artist_id); return true; } /** * Get all artist requests for a specific shop * * @param int $shop_id Shop term ID * @param string $status Optional status filter ('requested', 'accepted', 'rejected') * @return array Array of request objects with artist data */ public function getShopArtistRequests(int $shop_id, string $status = ''):array { global $wpdb; $table = $wpdb->prefix . BASE . 'artist_shop_requests'; // Build query $query = "SELECT r.*, u.display_name, p.post_title as artist_name FROM $table r JOIN {$wpdb->users} u ON r.user_id = u.ID JOIN {$wpdb->posts} p ON r.artist_id = p.ID WHERE r.shop_id = %d"; $params = [$shop_id]; // Add status filter if specified if ($status !== '') { $query .= " AND r.status = %s"; $params[] = $status; } // Order by most recent first $query .= " ORDER BY r.created_date DESC"; // Get results $results = $wpdb->get_results( $wpdb->prepare($query, $params) ); if (!$results) { return []; } // Format the data for easier use $requests = []; foreach ($results as $row) { $requests[] = [ 'id' => $row->id, 'user_id' => $row->user_id, 'artist_id' => $row->artist_id, 'artist_name' => $row->artist_name, 'display_name' => $row->display_name, 'status' => $row->status, 'created_date' => $row->created_date, 'updated_date' => $row->updated_date, 'notes' => $row->notes, 'managers' => $row->managers ? json_decode($row->managers, true) : null ]; } return $requests; } /** * @param int $shopID * @param int $userID * @param int $artistID * * @return void */ private function notifyShopManagers(int $shopID, int $userID, int $artistID):void { if (!$this->checkShop($shopID) || !$this->checkUser($userID)) { return; } // Get shop managers $shopMeta = Meta::forTerm($shopID, 'term'); $owners = $shopMeta->getAll(['managers', 'owner']); $owners = array_unique(array_merge($owners['managers'], $owners['owner'])); // Get artist name $artist_name = get_the_title($artistID); $shop_name = get_term($shopID, BASE . 'shop')->name; // Notify each manager foreach ($owners as $owner) { JVB()->notification()->addNotification( $owner, 'artist_request', [ 'artist_id' => $artistID, 'artist_name' => $artist_name, 'user_id' => $userID, 'shop_id' => $shopID, 'shop_name' => $shop_name ] ); } } /** * @param WP_REST_Request $request * * @return WP_REST_Response */ public function handleShopActions(WP_REST_Request $request):WP_REST_Response { $action = $request->get_param('action'); $shop_id = (int)$request->get_param('shop'); $userID = (int)$request->get_param('user'); if (!$this->checkShop($shop_id) || !$this->checkUser($userID)) { return new WP_REST_Response([ 'success' => false, 'message' => 'Invalid shop or user' ]); } switch ($action) { case 'add_artist': $this->addArtistToShop($userID, $shop_id); break; case 'remove_artist': $this->removeArtistFromShop($userID, $shop_id); break; case 'add_invitation': $data = [ 'name' => sanitize_text_field($request->get_param('name')), 'email' => sanitize_email($request->get_param('email')), ]; $this->createShopInvite($userID, $shop_id, $data); break; case 'remove_invitation': $target_id = sanitize_text_field($request->get_param('target_user')); $this->removeShopInvite($userID, $shop_id, $target_id); break; case 'process_request': $request_id = $request->get_param('request_id'); $decision = $request->get_param('decision'); // 'accept' or 'reject' $notes = $request->get_param('notes'); $this->processShopRequest($request_id, $decision, $notes, $userID); break; } return new WP_REST_Response([ 'success' => false, 'message' => 'Hmm... somehow you slipped through' ]); } /** * Create a shop invitation for an artist * * @param int $manager_id User ID creating the invitation (must be shop manager) * @param int $shop_id Shop ID to invite artist to * @param array $invite_data Invitation data (email, name, etc.) * @return array|WP_Error Result with success/error message */ public function createShopInvite(int $manager_id, int $shop_id, array $invite_data):array|WP_Error { // Validate parameters if (!$this->checkUser($manager_id) || !$this->checkShop($shop_id)) { return new WP_Error('invalid_parameters', 'Invalid user or shop ID'); } // Check if user has shop management permission if (!user_can($manager_id, 'manage_shop_' . $shop_id)) { return new WP_Error('permission_denied', 'You do not have permission to manage this shop'); } // Extract and validate artist data $artist_email = sanitize_email($invite_data['email'] ?? ''); $artist_name = sanitize_text_field($invite_data['name'] ?? ''); if (empty($artist_email)) { return new WP_Error('missing_email', 'Artist email is required'); } global $wpdb; $table = $wpdb->prefix . $this->table; // Check if invitation already exists for this email $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table WHERE email = %s AND to_shop = %d AND status = 'pending'", $artist_email, $shop_id )); if ($existing) { return new WP_Error('duplicate_invitation', 'An invitation has already been sent to this email'); } // Check if user with this email already exists $existing_user_id = email_exists($artist_email); // Generate a unique token $token = wp_generate_password(64, false); // Set expiration (30 days from now) $expires_at = date('Y-m-d H:i:s', strtotime('+30 days')); // Insert the invitation $result = $wpdb->insert( $table, [ 'name' => $artist_name, 'email' => $artist_email, 'invitation_token' => $token, 'inviters' => json_encode([$manager_id]), 'to_shop' => $shop_id, 'expires_at' => $expires_at, 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql') ] ); if (!$result) { return new WP_Error('db_error', 'Failed to create invitation: ' . $wpdb->last_error); } $invitation_id = $wpdb->insert_id; // If user exists, send notification through the system if ($existing_user_id) { JVB()->notification()->addNotification( $existing_user_id, 'artist_invitation', $manager_id, sprintf('You have been invited to join %s', get_term($shop_id, BASE . 'shop')->name), $invitation_id, 'invitation' ); } // Send email invitation $this->sendInvitationEmail($artist_email, $artist_name, $manager_id, $shop_id, $token); return [ 'success' => true, 'invitation_id' => $invitation_id, 'message' => 'Invitation sent successfully' . ($existing_user_id ? ' and notification delivered' : '') ]; } /** * Send invitation email to artist * * @param string $email Recipient email * @param string $name Recipient name * @param int $inviter_id User ID of inviter * @param int $shop_id Shop ID * @param string $token Invitation token * @return bool Success or failure */ protected function sendInvitationEmail(string $email, string $name, int $inviter_id, int $shop_id, string $token):bool { $inviter_name = jvbGetUsername($inviter_id); $shop_name = get_term($shop_id, BASE . 'shop')->name; $invitation_url = add_query_arg([ 'action' => 'accept_invitation', 'token' => $token ], home_url('/join/')); $subject = sprintf('Invitation to join %s on Edmonton Ink', $shop_name); $message = sprintf( 'Hello %s,

%s has invited you to join %s on Edmonton Ink.

Edmonton Ink is a community platform to connect tattoo artists and shops in Edmonton with enthusiasts.

Accept Invitation

Or copy and paste this link: %s

This invitation expires in 30 days.

♡ the edmonton.ink crew', esc_html($name), esc_html($inviter_name), esc_html($shop_name), esc_url($invitation_url), esc_url($invitation_url) ); return JVB()->email()->sendEmail($email, $subject, $message); } /** * @param int $manager_id * @param int $shop_id * @param int $userID * * @return WP_REST_Response */ public function removeShopInvite(int $manager_id, int $shop_id, int $userID):WP_REST_Response { if (!$this->checkUser($manager_id) || !$this->checkShop($shop_id)) { return new WP_REST_Response([ 'success' => false, 'message' => 'Invalid user or shop' ]); } // Check if user has shop management permission if (!user_can($manager_id, 'manage_shop_' . $shop_id)) { return new WP_REST_Response([ 'success' => false, 'message' => 'You do not have permission to manage this shop' ]); } global $wpdb; $table = $wpdb->prefix . $this->table; // Verify the invitation belongs to this shop $invitation = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table WHERE new_user_id = %d AND to_shop = %d AND status = 'pending'", $userID, $shop_id )); if (!$invitation) { return new WP_REST_Response([ 'success' => false, 'message' => 'Invitation not found or already processed' ]); } // Update invitation status to revoked $result = $wpdb->update( $table, [ 'status' => 'revoked', 'updated_at' => current_time('mysql') ], ['id' => $invitation->id] ); if ($result === false) { return new WP_REST_Response([ 'success' => false, 'message' => 'Failed to revoke invitation' ]); } // If invitation was for an existing user, notify them $user_id = email_exists($invitation->email); if ($user_id) { JVB()->notification()->addNotification( $user_id, 'invitation_revoked', $manager_id, sprintf('Your invitation to %s has been revoked', get_term($shop_id, BASE . 'shop')->name), $shop_id, BASE . 'shop' ); } return new WP_REST_Response([ 'success' => true, 'message' => 'Invitation successfully revoked' ]); } /** * @param int $request_id * @param string $decision * @param string $notes * @param int $manager_id * * @return WP_REST_Response */ protected function processShopRequest(int $request_id, string $decision, string $notes, int $manager_id):WP_REST_Response { global $wpdb; $table = $wpdb->prefix . BASE . 'artist_shop_requests'; // Get request details $request = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table WHERE id = %d", $request_id )); if (!$request) { return new WP_REST_Response([ 'success' => false, 'message' => 'Request not found' ]); } // Check if the current user has permission to manage this shop if (!user_can($manager_id, 'manage_shop_' . $request->shop_id)) { return new WP_REST_Response([ 'success' => false, 'message' => 'You do not have permission to manage this shop' ]); } // Update request status $status = $decision === 'accept' ? 'accepted' : 'rejected'; $wpdb->update( $table, [ 'status' => $status, 'updated_date' => current_time('mysql') ], ['id' => $request_id] ); // If accepted, add artist to shop if ($status === 'accepted') { $this->addArtistToShop($request->user_id, $request->shop_id); } // Notify the artist $notification_type = $status === 'accepted' ? 'shop_accepted' : 'shop_rejected'; JVB()->notification()->addNotification( $request->user_id, $notification_type, [ 'shop_id' => $request->shop_id, 'shop_name' => get_term($request->shop_id, BASE . 'shop')->name, 'notes' => $notes ] ); return new WP_REST_Response([ 'success' => true, 'message' => 'Request has been ' . $status ]); } /** * Get shop invitation details * * @param int $user_id The user ID who received the invitation * @param int $shop_id The shop ID they were invited to * @return array|false Invitation details or false if not found */ protected function getShopInvitation(int $user_id, int $shop_id):array|false { if (!$this->checkUser($user_id) || !$this->checkShop($shop_id)) { return false; } global $wpdb; $table = $wpdb->prefix . $this->table; // Get the invitation record by email $user = get_userdata($user_id); if (!$user || empty($user->user_email)) { return false; } $invitation = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table WHERE email = %s AND to_shop = %d AND status = 'pending' AND expires_at > NOW() ORDER BY created_at DESC LIMIT 1", $user->user_email, $shop_id ), ARRAY_A ); if (!$invitation) { return false; } // Parse inviters JSON field if (!empty($invitation['inviters'])) { $inviters = json_decode($invitation['inviters'], true); $invitation['invited_by'] = $inviters[0] ?? null; // Get the first inviter } return $invitation; } /** * Accept artist invitation * * Purpose: For an artist to accept an invitation to a shop * * @param int $user_id User ID * @param int $shopID Shop ID * * @return boolean Success or failure */ public function acceptShopInvitation(int $user_id, int $shopID):bool { if (!$this->checkUser($user_id) || !$this->checkShop($shopID)) { return false; } // Get the invitation details $invitation = $this->getShopInvitation($user_id, $shopID); if (!$invitation) { // Could not find invitation - may have expired or been processed already return false; } global $wpdb; $table = $wpdb->prefix . $this->table; // Start transaction $wpdb->query('START TRANSACTION'); try { $added = $this->addArtistToShop($user_id, $shopID); if (!$added) { throw new Exception("Failed to add artist to shop"); } // Update invitation record $updated = $wpdb->update( $table, [ 'status' => 'accepted', 'new_user_id' => $user_id, 'accepted_at' => current_time('mysql'), 'updated_at' => current_time('mysql') ], ['id' => $invitation['id']] ); if ($updated === false) { throw new Exception("Failed to update invitation status"); } // Notify inviters $inviters = json_decode($invitation['inviters'], true); if (!empty($inviters)) { foreach ($inviters as $inviter_id) { JVB()->notification()->addNotification( $inviter_id, 'invitation_accepted', [ 'artist_id' => $user_id, 'artist_name' => jvbGetUsername($user_id), 'shop_id' => $invitation['to_shop'] ] ); } } $wpdb->query('COMMIT'); return true; } catch (Exception $e) { $wpdb->query('ROLLBACK'); JVB()->error()->log( 'shop', "Error accepting artist invitation: " . $e->getMessage(), [ 'user_id' => $user_id, 'invitation' => $invitation ], 'error' ); return false; } } /** * Decline artist invitation * Purpose: for artists to decline an invitation to join a shop * * @param int $user_id User ID * @param int $shop_id Shop ID * * @return boolean Success or failure */ protected function declineShopInvitation( int $user_id, int $shop_id ):bool { if (!$this->checkUser($user_id) || !$this->checkShop($shop_id)) { return false; } // Get the invitation details $invitation = $this->getShopInvitation($user_id, $shop_id); if (!$invitation) { // Could not find invitation - may have expired or been processed already return false; } global $wpdb; $table = $wpdb->prefix . $this->table; try { // Update invitation record $wpdb->update( $table, [ 'status' => 'rejected', 'updated_at' => current_time('mysql') ], ['id' => $invitation['id']] ); // Notify inviters $inviters = json_decode($invitation['inviters'], true); if (!empty($inviters)) { foreach ($inviters as $inviter_id) { JVB()->notification()->addNotification( $inviter_id, 'invitation_declined', [ 'artist_id' => $user_id, 'artist_name' => jvbGetUsername($user_id), 'shop_id' => $invitation['to_shop'] ] ); } } return true; } catch (Exception $e) { JVB()->error()->log( 'shop', "Error declining artist invitation: " . $e->getMessage(), [ 'user_id' => $user_id, 'invitation' => $invitation ], 'error' ); return false; } } /** * Add artist to shop with proper validation and transactions * @param int $user_id User ID * @param int $shop_id Shop ID * @return boolean Success or failure */ protected function addArtistToShop(int $user_id, int $shop_id):bool { global $wpdb; $table = $wpdb->prefix . BASE . 'artist_shop_history'; // Start transaction $wpdb->query('START TRANSACTION'); try { // Check if already associated $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table WHERE user_id = %d AND shop_id = %d AND end_date IS NULL", $user_id, $shop_id )); if ($existing) { $wpdb->query('COMMIT'); return true; // Already associated } // Get the artist's post ID $artist_id = get_user_meta($user_id, BASE . 'link', true); if (!$artist_id) { throw new Exception("Artist profile not found"); } // Verify artist post exists $artist_post = get_post($artist_id); if (!$artist_post) { throw new Exception("Artist profile post not found"); } // Insert new association $result = $wpdb->insert( $table, [ 'user_id' => $user_id, 'artist_id' => $artist_id, 'shop_id' => $shop_id, 'role' => 'artist', 'is_primary' => 1, 'start_date' => current_time('mysql'), 'created_at' => current_time('mysql') ] ); if ($result === false) { throw new Exception("Failed to insert shop association: " . $wpdb->last_error); } // Add the taxonomy term to the artist post using WordPress API $term_result = wp_set_object_terms($artist_id, [$shop_id], BASE . 'shop', true); if (is_wp_error($term_result)) { throw new Exception("Failed to set shop term: " . $term_result->get_error_message()); } $wpdb->query('COMMIT'); return true; } catch (Exception $e) { $wpdb->query('ROLLBACK'); JVB()->error()->logError("Error adding artist to shop: " . $e->getMessage(), [ 'user_id' => $user_id, 'shop_id' => $shop_id ]); return false; } } /** * Remove artist from shop * * @param int $user_id User ID * @param int $shop_id Shop ID * @return bool Success or failure */ protected function removeArtistFromShop(int $user_id, int $shop_id): bool { if (!$this->checkUser($user_id) || !$this->checkShop($shop_id)) { return false; } global $wpdb; $table = $wpdb->prefix . BASE . 'artist_shop_history'; // Start transaction $wpdb->query('START TRANSACTION'); try { // Get the artist's post ID $artist_id = get_user_meta($user_id, BASE . 'link', true); if (!$artist_id) { throw new Exception("Artist profile not found"); } // Update the existing active association to set end_date $updated = $wpdb->update( $table, [ 'end_date' => current_time('mysql'), 'is_primary' => 0 ], [ 'user_id' => $user_id, 'shop_id' => $shop_id, 'end_date' => null ] ); if ($updated === false) { throw new Exception("Failed to update shop association: " . $wpdb->last_error); } // Remove the shop term from the artist post $term_result = wp_remove_object_terms($artist_id, [$shop_id], BASE . 'shop'); if (is_wp_error($term_result)) { throw new Exception("Failed to remove shop term: " . $term_result->get_error_message()); } $wpdb->query('COMMIT'); // Create a notification for the shop owner/manager $shop_name = get_term($shop_id, BASE . 'shop')->name; $shop_meta = Meta::forTerm($shop_id); $owners = $shop_meta->getAll(['owner', 'managers']); $owners = array_unique(array_merge(explode(',', $owners['owner']),explode(',', $owners['managers']))); foreach ($owners as $owner_id) { if (!empty($owner_id)) { JVB()->notification()->addNotification( $owner_id, 'artist_left', $user_id, sprintf('%s has left %s', jvbGetUsername($user_id), $shop_name), $shop_id, BASE . 'shop' ); } } return true; } catch (Exception $e) { $wpdb->query('ROLLBACK'); JVB()->error()->log( 'shop', "Error removing artist from shop: " . $e->getMessage(), [ 'user_id' => $user_id, 'shop_id' => $shop_id ], 'error' ); return false; } } /** * Get all artist user IDs currently at a specific shop * * @param int $shop_id Shop term ID * @param string $return Whether to return post artist ids or user ids * @return array Array of user IDs */ public function getShopArtistIds(int $shop_id, string $return = 'user_id'): array { if (!$this->checkShop($shop_id)) { return []; } if (!in_array($return, ['user_id', 'artist_id'])) { $return = 'user_id'; } global $wpdb; $table = $wpdb->prefix . BASE . 'artist_shop_history'; // Get just the user_ids of active artists at this shop $artists = $wpdb->get_col($wpdb->prepare( "SELECT %s FROM $table WHERE shop_id = %d AND end_date IS NULL ORDER BY is_primary DESC, created_at ASC", $return, $shop_id )); return $artists ?: []; } /** * @param int $user * @param int $shop * @param bool $grant * * @return bool */ public function setShopOwner(int $user, int $shop, bool $grant = true):bool { if (!$this->checkUser($user) || !$this->checkShop($shop)) { return false; } return $this->setManagerPermissions('owner', $user, $shop, $grant); } //In case we want to differentiate between owners and folks with manage capability, we have this /** * @param int $user * @param int $shop * @param bool $grant * * @return bool */ public function setShopManager(int $user, int $shop, bool $grant = true):bool { if (!$this->checkUser($user) || !$this->checkShop($shop)) { return false; } return $this->setManagerPermissions('manager', $user, $shop, $grant); } /** * @param string $type * @param int $user * @param int $shop * @param bool $grant * * @return bool */ protected function setManagerPermissions(string $type, int $user, int $shop, bool $grant):bool { switch ($type) { case 'owner': $uMeta = 'owner_of'; $aMeta = 'shop_owner'; $sMeta = 'owner'; break; case 'manager': $uMeta = 'manager_of'; $aMeta = 'shop_manager'; $sMeta = 'managers'; break; default: return false; } $userLink = get_user_meta($user, BASE . 'link', true); $userMeta = Meta::forUser($user); $artistMeta = Meta::forPost($userLink); $shopMeta = Meta::forTerm($shop); error_log('Setting '.$type.' permissions:'.print_r([ 'userLink' => $userLink, 'user' => $user, 'shop' => $shop, ], true)); //Attempt to prevent shop owners from being removed prematurely if (!current_user_can('manage_options') && user_can((int)$user, 'manage_shop_'.$shop)) { return false; } $user = get_userdata($user); $user->add_cap('manage_shop', $grant); $user->add_cap('manage_shop_'.$shop, $grant); $shops = $userMeta->get($uMeta); $shops = ($shops === '') ? [] : explode(',', $shops); if ($grant) { if (!in_array($shop, $shops)) { $shops[] = $shop; } } else { if (in_array($shop, $shops)) { unset($shops[array_search($shop, $shops)]); } } $shops = implode(',', $shops); $userMeta->set($uMeta, $shops); $artistMeta->set($aMeta, $shops); $owners = $shopMeta->get($sMeta); $owners = ($owners === '') ? [] : explode(',', $shops); if (!in_array($user->ID, $owners)) { $owners[] = $user->ID; } $owners = implode(',', $owners); $shopMeta->set($sMeta, $owners); return true; } }