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