<?php
|
namespace JVBase\rest\routes;
|
|
use JVBase\JVB;
|
use JVBase\managers\ImageGenerator;
|
use JVBase\managers\UploadManager;
|
use JVBase\registrar\Registrar;
|
use JVBase\rest\RestRouteManager;
|
use JVBase\meta\Meta;
|
use WP_REST_Request;
|
use WP_REST_Response;
|
use WP_Error;
|
use Exception;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
|
/***
|
* @deprecated
|
* WORKFLOW:
|
* 1) Users with 'manage_shop' permissions can invite folks to this shop
|
* 2) If user is already a registered member:
|
* a) Send invitation to user
|
* b) Handle user action ('accept' or 'deny' invitation)
|
* c) If user accepts invitation, handle shop transition (update shop history table, add shop term to ei_artist post type)
|
* d) If user isn't verified, verify them
|
* 3) If user is NOT a registered member:
|
* a) generate invitation link to email
|
* b) store invitation as an id
|
*/
|
class ShopRoutes extends RestRouteManager
|
{
|
protected $table = BASE.'artist_invitations';
|
|
public function __construct()
|
{
|
$this->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,<br><br>
|
%s has invited you to join %s on Edmonton Ink.<br><br>
|
Edmonton Ink is a community platform to connect tattoo artists and shops in Edmonton with enthusiasts.<br><br>
|
<a href="%s" style="display: inline-block; padding: 10px 20px; background-color: #FF0080; color: white; text-decoration: none; border-radius: 5px;">Accept Invitation</a><br><br>
|
Or copy and paste this link: %s<br><br>
|
This invitation expires in 30 days.<br><br>
|
♡ 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;
|
}
|
}
|