=Still working away at the Integrations overhaul
9 files added
6 files modified
| New file |
| | |
| | | <?php |
| | | namespace JVBase\integrations; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class APIEndpoint |
| | | { |
| | | protected string|false $create = false; |
| | | protected string|false $update = false; |
| | | protected string|false $delete = false; |
| | | protected string|false $batchCreate = false; |
| | | protected string|false $batchUpdate = false; |
| | | protected string|false $batchDelete = false; |
| | | protected string|false $batchImport = false; |
| | | protected string|false $search = false; |
| | | protected string|false $image = false; |
| | | |
| | | public function __construct() { |
| | | |
| | | } |
| | | public function setCreate(string $create):void |
| | | { |
| | | $this->create = $create; |
| | | } |
| | | public function getCreate():string|false |
| | | { |
| | | return $this->create; |
| | | } |
| | | public function setUpdate(string $update):void |
| | | { |
| | | $this->update = $update; |
| | | } |
| | | public function getUpdate():string|false |
| | | { |
| | | return $this->update; |
| | | } |
| | | public function setDelete(string $delete):void |
| | | { |
| | | $this->delete = $delete; |
| | | } |
| | | public function getDelete():string|false |
| | | { |
| | | return $this->delete; |
| | | } |
| | | public function setSearch(string $search):void |
| | | { |
| | | $this->search = $search; |
| | | } |
| | | public function getSearch():string|false |
| | | { |
| | | return $this->search; |
| | | } |
| | | public function setImage(string $image):void |
| | | { |
| | | $this->image = $image; |
| | | } |
| | | public function getImage():string|false |
| | | { |
| | | return $this->image; |
| | | } |
| | | public function setBatchCreate(string $batchCreate):void |
| | | { |
| | | $this->batchCreate = $batchCreate; |
| | | } |
| | | public function getBatchCreate():string|false |
| | | { |
| | | return $this->batchCreate; |
| | | } |
| | | public function setBatchUpdate(string $batchUpdate):void |
| | | { |
| | | $this->batchUpdate = $batchUpdate; |
| | | } |
| | | public function getBatchUpdate():string|false |
| | | { |
| | | return $this->batchUpdate; |
| | | } |
| | | public function setBatchDelete(string $batchDelete):void |
| | | { |
| | | $this->batchDelete = $batchDelete; |
| | | } |
| | | public function getBatchDelete():string|false |
| | | { |
| | | return $this->batchDelete; |
| | | } |
| | | public function setImport(string $batchImport):void |
| | | { |
| | | $this->batchImport = $batchImport; |
| | | } |
| | | public function getBatchImport():string|false |
| | | { |
| | | return $this->batchImport; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\integrations; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class Endpoint { |
| | | protected string $endpoint; |
| | | protected array $fields; |
| | | public function __construct(string $endpoint) { |
| | | $this->endpoint = $endpoint; |
| | | } |
| | | |
| | | public function getEndpoint():string |
| | | { |
| | | return $this->endpoint; |
| | | } |
| | | |
| | | public function addIdempotencyKey(?string $key = null):EndpointField |
| | | { |
| | | if (is_null($key)) { |
| | | $key = wp_generate_uuid4(); |
| | | } |
| | | $field = new EndpointField('idempotency_key'); |
| | | $this->fields['idempotency_key'] = $field; |
| | | return $field; |
| | | } |
| | | |
| | | public function getFields():array |
| | | { |
| | | return $this->fields; |
| | | } |
| | | public function addField(string $slug, array $data = []):EndpointField |
| | | { |
| | | $slug = EndpointField::sanitizeSlug($slug); |
| | | $field = new EndpointField($slug); |
| | | if (!empty($data)) { |
| | | foreach ($data as $key => $value) { |
| | | if ($key === 'required' && $value !== false) { |
| | | $field->setRequired(); |
| | | } elseif (property_exists($field, $key)) { |
| | | $method = 'set'.implode('',array_map('ucfirst', explode('_',$key))); |
| | | if (method_exists($field, $method)) { |
| | | $field->$method($value); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | $this->fields[$slug] = $field; |
| | | return $field; |
| | | } |
| | | public function checkFields(array $fields):array|false |
| | | { |
| | | $fields = array_filter($fields, function ($f) use ($fields) { |
| | | if (!array_key_exists($f, $this->fields)) { |
| | | error_log('Endpoint '.$this->endpoint.'::checkFields removing field because it is not defined: '.print_r($fields[$f], true)); |
| | | return false; |
| | | } |
| | | return true; |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | $required = array_filter($this->fields, function ($f) { |
| | | return $f->isRequired(); |
| | | }); |
| | | |
| | | $missing = []; |
| | | foreach ($required as $field => $config) { |
| | | if (!array_key_exists($field, $fields) || empty($fields[$field])) { |
| | | error_log('Endpoint '.$this->endpoint.'::checkFields required field not found: '.$field); |
| | | $missing[] = $field; |
| | | } |
| | | } |
| | | if (!empty($missing)) { |
| | | return false; |
| | | } |
| | | |
| | | foreach ($fields as $field => $value) { |
| | | $test = $this->fields[$field]->validate($value); |
| | | if (!$test) { |
| | | error_log('Endpoint '.$this->endpoint.'::checkFields could not validate value for '.$field.': '.print_r($value, true)); |
| | | unset($fields[$field]); |
| | | } |
| | | } |
| | | |
| | | return $fields; |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\integrations; |
| | | |
| | | use Closure; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\meta\Validator; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | class EndpointField { |
| | | protected string $slug; |
| | | protected string $type = 'text'; |
| | | protected bool $required = false; |
| | | protected Closure $validation; |
| | | public function __construct(string $slug) |
| | | { |
| | | $this->slug = self::sanitizeSlug($slug); |
| | | } |
| | | |
| | | public function setRequired():void |
| | | { |
| | | $this->required = true; |
| | | } |
| | | public function isRequired():bool |
| | | { |
| | | return $this->required; |
| | | } |
| | | |
| | | public function setValidation(callable $validation):void |
| | | { |
| | | $this->validation = Closure::fromCallable($validation); |
| | | } |
| | | public function handleValidation(mixed $data):bool|null |
| | | { |
| | | if (is_callable($this->validation)) { |
| | | return ($this->validation)($data); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | public static function sanitizeSlug(string $slug):string |
| | | { |
| | | return str_replace('-','_', sanitize_title($slug)); |
| | | } |
| | | public function validate(mixed $value):bool |
| | | { |
| | | $result = $this->handleValidation($value); |
| | | if (is_null($result) && !empty($this->type)) { |
| | | error_log('Handling validation with the Meta Validator.php class'); |
| | | Validator::validate($value, ['type' => $this->type]); |
| | | } |
| | | error_log('No validation set for '.$this->slug.' endpoint field'); |
| | | return true; |
| | | } |
| | | } |
| | |
| | | * These properties define how the integration connects to external services |
| | | */ |
| | | protected string|array $apiBase = ''; // Base URL(s) for API endpoints. Array format: ['base' => '', 'auth' => ''] |
| | | /** |
| | | * @var array<APIEndpoint> An array of endpoints objects, with optional create, update, delete, batch<x>, and batchImport |
| | | */ |
| | | protected array $apiEndpoints = []; // Valid endpoint paths for this service |
| | | |
| | | protected int $refresh_interval = 0; //seconds before expiry to refresh tokens. 0 to disable |
| | |
| | | |
| | | $this->registerAdditionalQueueTypes($executor); |
| | | } |
| | | protected function registerAdditionalQueueTypes(IntegrationExecutor $executor):void |
| | | { |
| | | //Empty. Integration extensions can register additional operation types from here. |
| | | } |
| | | |
| | | /** |
| | | * @return void Sets the $this->contentTypes |
| | | * Empty. Integration extensions can register additional operation types here |
| | | * @param IntegrationExecutor $executor |
| | | * @return void |
| | | */ |
| | | protected function registerAdditionalQueueTypes(IntegrationExecutor $executor):void {} |
| | | |
| | | /** |
| | | * @return void Sets the $this->contentTypes, which is an array of TYPE => [$fields] |
| | | * Should probably set the endpoints here as well, which would be an array of [ TYPE => [ 'create' => '','update' =>'', 'delete'=>'', 'batchCreate'=>'','batchUpdate'=>'', 'batchDelete'=>''] endpoints |
| | | */ |
| | | abstract protected function setContentTypes():void; |
| | | } |
| | |
| | | exit; |
| | | } |
| | | |
| | | /** |
| | | * For Syncing to a service, the basic flow is: |
| | | * 1) Queuing an operation |
| | | * a) using the $syncTo as the operation type |
| | | * b) the $data has: |
| | | * i) key of 'posts', 'terms', 'users' depending on the source data |
| | | * ii) key of 'user' to set user's specific integration connection, or the site-wide connection |
| | | * 2) IntegrationExecutor then determines if it is a sync to operation |
| | | * -> from there, it calls the integrations {action}BatchToService or {create}OneToService |
| | | * |
| | | * Most integrations will just need to set the $hasBatchUpdate, $hasBatchDelete, $hasBatchCreate flags to signal we can condense it into a single request, |
| | | * set the formatForService() method if anything deviates from the defaults |
| | | * and the endpoint for a particular action |
| | | */ |
| | | trait SyncTo { |
| | | use SyncHelpers, UserConnection; |
| | | /** |
| | |
| | | * Defaults to an array of formatted items. Can be overridden in child classes |
| | | * @param array $itemIDs |
| | | * @param string $type |
| | | * @return array |
| | | * @return array Either an array of formatted items, or a single formatted item (if there is 1) |
| | | */ |
| | | protected function formatItems(array $itemIDs, string $type):array |
| | | { |
| | |
| | | ]); |
| | | } |
| | | } |
| | | return $items; |
| | | return count($items) === 1 ? $items[0] : $items; |
| | | } |
| | | |
| | | protected function determineType(array $data):string|false |
| | | { |
| | | $type = false; |
| | | if (array_key_exists('posts', $data)) { |
| | | $type = 'post'; |
| | | } else if (array_key_exists('terms', $data)) { |
| | | $type = 'term'; |
| | | } else if (array_key_exists('users', $data)) { |
| | | $type = 'user'; |
| | | } |
| | | return $type; |
| | | } |
| | | /*************************************************************** |
| | | * Item creation |
| | | ***************************************************************/ |
| | | public function createBatchToService($data):array |
| | | { |
| | | $type = $data['type']??'post'; |
| | | $type = $this->determineType($data); |
| | | |
| | | if (!$type) { |
| | | $this->logError('createBatchToService', 'No expected keys in data', ['data' => $data]); |
| | | return $this->noCreatedItems([]); |
| | | } |
| | | |
| | | //Check if any of the submitted ids are already created |
| | | $created = array_filter($data['items'], |
| | |
| | | } else { |
| | | $success = $errors = []; |
| | | foreach ($items as $item) { |
| | | $itemResult = $this->createOne($item); |
| | | $itemResult = $this->createOneToService($item); |
| | | if (!is_wp_error($itemResult)) { |
| | | $success[] = $itemResult; |
| | | } else { |
| | |
| | | * @param array $item |
| | | * @return array|WP_Error |
| | | */ |
| | | protected function createOne(array $item):array|WP_Error |
| | | protected function createOneToService(array $item):array|WP_Error |
| | | { |
| | | return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own createOne'); |
| | | return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own createOneToService'); |
| | | } |
| | | |
| | | /** |
| | |
| | | *****************************************************************/ |
| | | public function updateBatchToService(array $data):array |
| | | { |
| | | $type = $data['type']??'post'; |
| | | $type = $this->determineType($data); |
| | | |
| | | if (!$type) { |
| | | $this->logError('createBatchToService', 'No expected keys in data', ['data' => $data]); |
| | | return $this->noUpdatedItems([]); |
| | | } |
| | | |
| | | $newlyCreated = []; |
| | | if (!$this->canCreateOnUpdate) { |
| | |
| | | $success = $errors = []; |
| | | //Does not have batch update, manually update each one |
| | | foreach ($items as $item) { |
| | | $itemResult = $this->updateOne($item); |
| | | $itemResult = $this->updateOneToService($item); |
| | | if (!is_wp_error($itemResult)) { |
| | | $success[] = $itemResult; |
| | | } else { |
| | |
| | | * @param array $item |
| | | * @return array|WP_Error |
| | | */ |
| | | protected function updateOne(array $item):array|WP_Error |
| | | protected function updateOneToService(array $item):array|WP_Error |
| | | { |
| | | return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own updateOne method'); |
| | | return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own updateOneToService method'); |
| | | } |
| | | |
| | | /** |
| | |
| | | ] |
| | | ]; |
| | | } |
| | | |
| | | public function deleteOneToService(array $item):array|WP_Error |
| | | { |
| | | return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own deleteOneToService'); |
| | | } |
| | | public function sendBatchDelete(array $items):array|WP_Error |
| | | { |
| | | return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own sendBatchDelete'); |
| | | } |
| | | |
| | | public function deleteBatchToService(array $data):array |
| | | { |
| | | $type = $this->determineType($data); |
| | | |
| | | if (!$type) { |
| | | $this->logError('createBatchToService', 'No expected keys in data', ['data' => $data]); |
| | | return $this->noCreatedItems([]); |
| | | } |
| | | |
| | | //Check if any submitted ids do not have an integration item ID |
| | | $notCreated = array_filter($data['items'], |
| | | function($ID) use ($type) { |
| | | return empty($this->getServiceItemID($ID, $type)); |
| | | }); |
| | | if (!empty($notCreated)) { |
| | | $this->logError('deleteBatchToService','Could not delete items',['items' => $notCreated]); |
| | | } |
| | | |
| | | $created = array_filter($data['items'], |
| | | function($ID) use ($type) { |
| | | return !empty($this->getServiceItemID($ID, $type)); |
| | | }); |
| | | |
| | | if ($this->hasBatchDelete) { |
| | | $result = $this->sendBatchDelete($created); |
| | | } else { |
| | | $errors = $success = []; |
| | | foreach ($created as $item) { |
| | | try { |
| | | $success[] = $this->deleteOneToService($item); |
| | | } catch (Exception $e) { |
| | | $errors[] = $e->getMessage(); |
| | | } |
| | | } |
| | | |
| | | $result = [ |
| | | 'outcome' => empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial'), |
| | | 'result' => [ |
| | | 'success' => $success, |
| | | 'errors' => $errors |
| | | ] |
| | | ]; |
| | | } |
| | | return $result; |
| | | } |
| | | |
| | | /***************************************************************** |
| | | * UTILITY |
| | | *****************************************************************/ |
| | |
| | | return []; |
| | | } |
| | | |
| | | $content = match ($type) { |
| | | 'post' => get_post_type($itemID), |
| | | 'term' => get_term($itemID)?->taxonomy, |
| | | 'user' => jvbUserRole($itemID) |
| | | }; |
| | | $registrar = Registrar::getInstance($content); |
| | | if ($registrar) { |
| | | $additional = $this->getAdditionalFields($registrar->getIntegration($this->service_name)->getContentType()); |
| | | $additional = array_combine( |
| | | array_map(fn($k) => str_starts_with($k, '_'.$this->service_name) ? $k : "_{$this->service_name}_{$k}", array_keys($additional)), |
| | | $additional |
| | | ); |
| | | $additionalFields = array_merge($additionalFields, $additional); |
| | | } |
| | | |
| | | |
| | | $fields = [ |
| | | 'share_to_' . $this->service_name, |
| | | '_keep_synced_' . $this->service_name, |
| | |
| | | ]; |
| | | return $meta->getAll($fields); |
| | | } |
| | | /**************************************************************** |
| | | * POST SYNC |
| | | ****************************************************************/ |
| | | public function addSavePost():void |
| | | { |
| | | if (!has_action('save_post', [$this, 'handleSavePost'])) { |
| | | add_action('save_post', [$this, 'handleSavePost'], 20, 3); |
| | | } |
| | | } |
| | | public function removeSavePost():void |
| | | { |
| | | remove_action('save_post', [$this, 'handleSavePost'], 20, 3); |
| | | } |
| | | public function handleSavePost(int $postID, WP_Post $post, bool $update):void |
| | | { |
| | | if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; |
| | | if (wp_is_post_revision($postID)) return; |
| | | |
| | | error_log('=== ['.$this->service_name.']::handleSavePost called'); |
| | | |
| | | $postType = jvbNoBase($post->post_type); |
| | | if (!in_array($postType, $this->syncPostTypes)) { |
| | | error_log('Not handling save for '.$this->service_name.' because there are no syncPostTypes: '.print_r($this->syncPostTypes, true)); |
| | | return; |
| | | } |
| | | |
| | | $registrar = Registrar::getInstance($postType); |
| | | //Should not happen, as syncPostTypes is defined by Registrar instances |
| | | if (!$registrar) { |
| | | return; |
| | | } |
| | | |
| | | $settings = $registrar->getIntegrationConfig($this->service_name); |
| | | if (!$settings) { |
| | | error_log('Not handling save for '.$this->service_name.' because of no integration config '.print_r($settings, true)); |
| | | return; |
| | | } |
| | | |
| | | $fields = $this->getSyncFields($postID, 'post'); |
| | | if (!$fields['share_to_'.$this->service_name]) { |
| | | error_log('Not handling save for '.$this->service_name.' because of no share_to_'.$this->service_name.' '.print_r($fields, true)); |
| | | return; |
| | | } |
| | | |
| | | $isShared = array_key_exists("_{$this->service_name}_item_id", $fields) && !empty($fields["_{$this->service_name}_item_id"]); |
| | | |
| | | if ($post->post_status !== 'publish' && !$isShared) { |
| | | error_log('Not handling save for '.$this->service_name.' because post status is not publish, and it is not already shared.'); |
| | | return; |
| | | } |
| | | |
| | | if ($isShared && $update && !$fields['_keep_synced_'.$this->service_name]) { |
| | | error_log('Not handling save for '.$this->service_name.' because it is already shared, and not set to keep synced. '); |
| | | return; |
| | | } |
| | | error_log('==== Sending to integration\'s handleTheSavePost '.$this->service_name.' ===='); |
| | | $this->removeSavePost(); |
| | | $this->handleTheSavePost($postID, $post, $update, $settings); |
| | | $this->addSavePost(); |
| | | } |
| | | /** |
| | | * Handle post save for syncing |
| | | * |
| | | * Override to implement custom sync logic when posts are saved. |
| | | * Check the $settings array for post type specific configuration. |
| | | * |
| | | * @param int $postID The post ID |
| | | * @param WP_Post $post The post object |
| | | * @param bool $update Whether this is an update |
| | | * @param array $settings Post type integration settings |
| | | * @return void |
| | | */ |
| | | protected function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings):void |
| | | { |
| | | error_log('==== ['.$this->title.']::handleTheSavePost ===='); |
| | | $this->queueOperation(self::$syncTo, [ |
| | | 'posts' => [$postID], |
| | | 'user' => user_can($post->post_author, 'manage_options') ? null : $post->post_author |
| | | ], [ |
| | | 'priority' => 'high', |
| | | 'delay' => 30, |
| | | ]); |
| | | Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued'); |
| | | } |
| | | |
| | | public function addDeletePost():void |
| | | { |
| | | if (!has_action('before_delete_post', [$this, 'handleDeletePost'])) { |
| | | add_action('before_delete_post', [$this, 'handleDeletePost'], 20, 3); |
| | | } |
| | | } |
| | | public function removeDeletePost():void |
| | | { |
| | | remove_action('before_delete_post', [$this, 'handleSavePost'], 20, 3); |
| | | } |
| | | public function handleDeletePost(int $postID):void |
| | | { |
| | | if (!$this->canSync['delete']) { |
| | | return; |
| | | } |
| | | $postType = get_post_type($postID); |
| | | if (!in_array(jvbNoBase($postType), $this->syncPostTypes)) { |
| | | return; |
| | | } |
| | | $fields = $this->getSyncFields($postID, 'post'); |
| | | if (empty($fields["_{$this->service_name}_item_id"])) { |
| | | return; |
| | | } |
| | | $post = get_post($postID); |
| | | if (!$post) { |
| | | return; |
| | | } |
| | | $userID = $this->determineUserID($post->post_author); |
| | | if (!$userID) { |
| | | return; |
| | | } |
| | | |
| | | JVB()->queue()->add( |
| | | self::$deleteFrom, |
| | | $userID, |
| | | [ |
| | | 'fields' => [$postID => $fields], |
| | | 'service' => $this->service_name, |
| | | 'type' => 'post' |
| | | ] |
| | | ); |
| | | } |
| | | /**************************************************************** |
| | | * TERM SYNC |
| | | ****************************************************************/ |
| | | public function addSaveTerm():void |
| | | { |
| | | if (empty($this->syncTaxonomies)) { |
| | | return; |
| | | } |
| | | if (!has_action('saved_term', [$this, 'handleSaveTerm'])) { |
| | | add_action('saved_term', [$this, 'handleSaveTerm'], 20, 3); |
| | | } |
| | | } |
| | | public function removeSaveTerm():void |
| | | { |
| | | if (empty($this->syncTaxonomies)) { |
| | | return; |
| | | } |
| | | remove_action('saved_term', [$this, 'handleSaveTerm'], 20, 3); |
| | | } |
| | | protected function handleSaveTerm(int $termID, int $tt_id, string $taxonomy, bool $update, array $args):void |
| | | { |
| | | $tax = jvbNoBase($taxonomy); |
| | | if (!in_array($tax, $this->syncTaxonomies)) { |
| | | return; |
| | | } |
| | | $registrar = Registrar::getInstance($tax); |
| | | if (!$registrar) { |
| | | return; |
| | | } |
| | | |
| | | $settings = $registrar->getIntegrationConfig($this->service_name); |
| | | if (!$settings) { |
| | | return; |
| | | } |
| | | |
| | | $fields = $this->getSyncFields($termID, 'term'); |
| | | if (!$fields['share_to_'.$this->service_name]) { |
| | | return; |
| | | } |
| | | |
| | | $isShared = array_key_exists("_{$this->service_name}_item_id", $fields) && !empty($fields["_{$this->service_name}_item_id"]); |
| | | |
| | | if ($isShared && $update && !$fields['_keep_synced_'.$this->service_name]) { |
| | | return; |
| | | } |
| | | $this->removeSaveTerm(); |
| | | $this->handleTheSaveTerm($termID, $update, $taxonomy, $settings); |
| | | $this->addSaveTerm(); |
| | | } |
| | | |
| | | /** |
| | | * @param int $termID |
| | | * @param bool $update |
| | | * @param array $settings |
| | | * @return void |
| | | */ |
| | | protected function handleTheSaveTerm(int $termID, bool $update, string $taxonomy, array $settings):void |
| | | { |
| | | error_log('==== ['.$this->title.']::handleTheSaveTerm ===='); |
| | | //TODO: Figure out some sort of permissions for if the user can share this term, particularly for content types |
| | | //TODO: If this is a content type, it likely has its own integration. This is an edmonton.ink problem, so I'm offloading it for now |
| | | $this->queueOperation(self::$syncTo, [ |
| | | 'terms' => [$termID], |
| | | 'user' => get_current_user_id(), |
| | | ], [ |
| | | 'priority' => 'high', |
| | | 'delay' => 30, |
| | | ]); |
| | | Meta::forTerm($termID)->set('_'.$this->service_name.'_sync_status', 'queued'); |
| | | } |
| | | |
| | | public function addDeleteTerm():void |
| | | { |
| | | if (!has_action('pre_delete_term', [$this, 'handleDeleteTerm'])) { |
| | | add_action('pre_delete_term', [$this, 'handleDeleteTerm']); |
| | | } |
| | | } |
| | | public function removeDeleteTerm():void |
| | | { |
| | | remove_action('pre_delete_term', [$this, 'handleDeleteTerm']); |
| | | } |
| | | public function handleDeleteTerm(int $termID, string $taxonomy):void |
| | | { |
| | | if (!$this->canSync['delete']) { |
| | | return; |
| | | } |
| | | $tax = jvbNoBase($taxonomy); |
| | | if (!in_array($tax, $this->syncTaxonomies)) { |
| | | return; |
| | | } |
| | | $fields = $this->getSyncFields($termID, 'term'); |
| | | if (empty($fields["_{$this->service_name}_item_id"])) { |
| | | return; |
| | | } |
| | | JVB()->queue()->add( |
| | | self::$deleteFrom, |
| | | 0, |
| | | [ |
| | | 'fields' => [$termID => $fields], |
| | | 'service' => $this->service_name, |
| | | 'type' => 'term' |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | /**************************************************************** |
| | | * USER SYNC |
| | | ****************************************************************/ |
| | | public function addSaveUser():void |
| | | { |
| | | if (empty($this->syncUsers)) { |
| | | return; |
| | | } |
| | | if (!has_action('profile_update', [$this, 'handleUpdateUser'])) { |
| | | add_action('profile_update', [$this, 'handleUpdateUser'], 20, 1); |
| | | } |
| | | if (!has_action('user_register', [$this, 'handleUpdateUser'])) { |
| | | add_action('user_register', [$this, 'handleUpdateUser'], 20, 1); |
| | | } |
| | | } |
| | | public function removeSaveUser():void |
| | | { |
| | | if (empty($this->syncUsers)) { |
| | | return; |
| | | } |
| | | remove_action('profile_update', [$this, 'handleUpdateUser'], 20); |
| | | remove_action('user_register', [$this, 'handleUpdateUser'], 20); |
| | | } |
| | | protected function handleUpdateUser(int $userID):void |
| | | { |
| | | $user = $this->getOrCreateUser($userID); |
| | | if (!$user) { |
| | | return; |
| | | } |
| | | |
| | | $fields = $this->getSyncFields($userID, 'user'); |
| | | if (!empty($fields["_{$this->service_name}_item_id"])) { |
| | | return; |
| | | } |
| | | |
| | | $this->removeSaveUser(); |
| | | $this->handleTheSaveUser($userID); |
| | | $this->addSaveTerm(); |
| | | } |
| | | |
| | | protected function getOrCreateUser(int $userID):string|false |
| | | { |
| | | $user = get_userdata($userID); |
| | | if (!$user || is_wp_error($user)) { |
| | | return false; |
| | | } |
| | | $role = jvbUserRole($userID); |
| | | if (!in_array(jvbNoBase($role), $this->syncUsers)) { |
| | | return false; |
| | | } |
| | | $fields = $this->getSyncFields($userID, 'user'); |
| | | if (!empty($fields["_{$this->service_name}_item_id"])) { |
| | | return $fields["_{$this->service_name}_item_id"]; |
| | | } |
| | | |
| | | $meta = Meta::forUser($userID); |
| | | $serviceUserID = $this->searchServiceForUser($user->user_email??''); |
| | | if ($serviceUserID) { |
| | | $meta->set("{$this->service_name}_item_id", $serviceUserID); |
| | | return $serviceUserID; |
| | | } |
| | | |
| | | $created = $this->createServiceUser($userID, $fields); |
| | | if ($created) { |
| | | return $created; |
| | | } |
| | | return false; |
| | | } |
| | | public function searchServiceForUser(string $email):string|false |
| | | { |
| | | if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { |
| | | return false; |
| | | } |
| | | return $this->handleEmailSearch($email); |
| | | } |
| | | |
| | | /** |
| | | * Searches for existing user from sanitized email |
| | | * @param string $email |
| | | * @return string|false The found service's User ID or false on failure |
| | | */ |
| | | abstract public function handleEmailSearch(string $email):string|false; |
| | | |
| | | public function createServiceUser(int $userID, array $fields):string|false |
| | | { |
| | | $checked = $this->validateUserFields($userID, $fields); |
| | | $response = $this->handleCreateUser($checked); |
| | | if ($response['success']) { |
| | | $meta = Meta::forUser($userID); |
| | | $meta->set("{$this->service_name}_item_id", $response['result']['id']); |
| | | } |
| | | return $response['success'] ? $response['customer']??false : false; |
| | | } |
| | | |
| | | protected function validateUserFields(int $userID, array $fields):array|false |
| | | { |
| | | foreach ($fields as $f => $v) { |
| | | $v = match ($f) { |
| | | 'email' => filter_var($v, FILTER_SANITIZE_EMAIL), |
| | | 'phone' => Sanitizer::sanitizePhone($v), |
| | | default => sanitize_text_field($v), |
| | | }; |
| | | if (empty($v) && in_array($f, $this->requiredUserFields())) { |
| | | return false; |
| | | } |
| | | $fields[$f] = $v; |
| | | } |
| | | return $fields; |
| | | } |
| | | protected function requiredUserFields():array |
| | | { |
| | | return []; |
| | | } |
| | | |
| | | /** |
| | | * @param array $data User Data |
| | | * @return array The created user ID or false on failure |
| | | */ |
| | | abstract protected function handleCreateUser(array $data):array; |
| | | |
| | | /** |
| | | * @param int $userID |
| | | * @return void |
| | | */ |
| | | protected function handleTheSaveUser(int $userID):void |
| | | { |
| | | error_log('==== ['.$this->title.']::handleTheSaveUser ===='); |
| | | //TODO: This is likely only for stuff like customers. |
| | | //If we have multiple stores connected, we may have to queue operations to update every connection's customer account if they made an order with that store |
| | | $role = jvbUserRole($userID); |
| | | $registrar = Registrar::getInstance($role); |
| | | if (!$registrar || !$registrar->hasIntegration($this->service_name)) { |
| | | return; |
| | | } |
| | | |
| | | $this->queueOperation(self::$syncTo, [ |
| | | 'users' => [$userID], |
| | | ]); |
| | | } |
| | | |
| | | public function addDeleteUser():void |
| | | { |
| | | if (!has_action('delete_user', [$this, 'handleDeleteTerm'])) { |
| | | add_action('delete_user', [$this, 'handleDeleteTerm']); |
| | | } |
| | | } |
| | | public function removeDeleteUser():void |
| | | { |
| | | remove_action('delete_user', [$this, 'handleDeleteTerm']); |
| | | } |
| | | public function handleDeleteUser(int $userID):void |
| | | { |
| | | if (!$this->canSync['delete']) { |
| | | return; |
| | | } |
| | | |
| | | $fields = $this->getSyncFields($userID, 'user'); |
| | | if (empty($fields["_{$this->service_name}_item_id"])) { |
| | | return; |
| | | } |
| | | JVB()->queue()->add( |
| | | self::$deleteFrom, |
| | | 0, |
| | | [ |
| | | 'fields' => [$userID => $fields], |
| | | 'service' => $this->service_name, |
| | | 'type' => 'user' |
| | | ] |
| | | ); |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\integrations; |
| | | |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | use WP_Post; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | trait SyncToPost |
| | | { |
| | | use SyncTo, SyncHelpers, UserConnection; |
| | | |
| | | public function addSavePost():void |
| | | { |
| | | if (!has_action('save_post', [$this, 'handleSavePost'])) { |
| | | add_action('save_post', [$this, 'handleSavePost'], 20, 3); |
| | | } |
| | | } |
| | | public function removeSavePost():void |
| | | { |
| | | remove_action('save_post', [$this, 'handleSavePost'], 20, 3); |
| | | } |
| | | public function handleSavePost(int $postID, WP_Post $post, bool $update):void |
| | | { |
| | | if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; |
| | | if (wp_is_post_revision($postID)) return; |
| | | |
| | | error_log('=== ['.$this->service_name.']::handleSavePost called'); |
| | | |
| | | $postType = jvbNoBase($post->post_type); |
| | | if (!in_array($postType, $this->syncPostTypes)) { |
| | | error_log('Not handling save for '.$this->service_name.' because there are no syncPostTypes: '.print_r($this->syncPostTypes, true)); |
| | | return; |
| | | } |
| | | |
| | | $registrar = Registrar::getInstance($postType); |
| | | //Should not happen, as syncPostTypes is defined by Registrar instances |
| | | if (!$registrar) { |
| | | return; |
| | | } |
| | | |
| | | $settings = $registrar->getIntegrationConfig($this->service_name); |
| | | if (!$settings) { |
| | | error_log('Not handling save for '.$this->service_name.' because of no integration config '.print_r($settings, true)); |
| | | return; |
| | | } |
| | | |
| | | $fields = $this->getSyncFields($postID, 'post'); |
| | | if (!$fields['share_to_'.$this->service_name]) { |
| | | error_log('Not handling save for '.$this->service_name.' because of no share_to_'.$this->service_name.' '.print_r($fields, true)); |
| | | return; |
| | | } |
| | | |
| | | $isShared = array_key_exists("_{$this->service_name}_item_id", $fields) && !empty($fields["_{$this->service_name}_item_id"]); |
| | | |
| | | if ($post->post_status !== 'publish' && !$isShared) { |
| | | error_log('Not handling save for '.$this->service_name.' because post status is not publish, and it is not already shared.'); |
| | | return; |
| | | } |
| | | |
| | | if ($isShared && $update && !$fields['_keep_synced_'.$this->service_name]) { |
| | | error_log('Not handling save for '.$this->service_name.' because it is already shared, and not set to keep synced. '); |
| | | return; |
| | | } |
| | | error_log('==== Sending to integration\'s handleTheSavePost '.$this->service_name.' ===='); |
| | | $this->removeSavePost(); |
| | | $this->handleTheSavePost($postID, $post, $update, $settings); |
| | | $this->addSavePost(); |
| | | } |
| | | /** |
| | | * Handle post save for syncing |
| | | * |
| | | * Override to implement custom sync logic when posts are saved. |
| | | * Check the $settings array for post type specific configuration. |
| | | * |
| | | * @param int $postID The post ID |
| | | * @param WP_Post $post The post object |
| | | * @param bool $update Whether this is an update |
| | | * @param array $settings Post type integration settings |
| | | * @return void |
| | | */ |
| | | protected function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings):void |
| | | { |
| | | error_log('==== ['.$this->title.']::handleTheSavePost ===='); |
| | | $this->queueOperation(self::$syncTo, [ |
| | | 'posts' => [$postID], |
| | | 'user' => user_can($post->post_author, 'manage_options') ? null : $post->post_author |
| | | ], [ |
| | | 'priority' => 'high', |
| | | 'delay' => 30, |
| | | ]); |
| | | Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued'); |
| | | } |
| | | |
| | | public function addDeletePost():void |
| | | { |
| | | if (!has_action('before_delete_post', [$this, 'handleDeletePost'])) { |
| | | add_action('before_delete_post', [$this, 'handleDeletePost'], 20, 3); |
| | | } |
| | | } |
| | | public function removeDeletePost():void |
| | | { |
| | | remove_action('before_delete_post', [$this, 'handleSavePost'], 20, 3); |
| | | } |
| | | public function handleDeletePost(int $postID):void |
| | | { |
| | | if (!$this->canSync['delete']) { |
| | | return; |
| | | } |
| | | $postType = get_post_type($postID); |
| | | if (!in_array(jvbNoBase($postType), $this->syncPostTypes)) { |
| | | return; |
| | | } |
| | | $fields = $this->getSyncFields($postID, 'post'); |
| | | if (empty($fields["_{$this->service_name}_item_id"])) { |
| | | return; |
| | | } |
| | | $post = get_post($postID); |
| | | if (!$post) { |
| | | return; |
| | | } |
| | | $userID = $this->determineUserID($post->post_author); |
| | | if (!$userID) { |
| | | return; |
| | | } |
| | | |
| | | JVB()->queue()->add( |
| | | self::$deleteFrom, |
| | | $userID, |
| | | [ |
| | | 'fields' => [$postID => $fields], |
| | | 'service' => $this->service_name, |
| | | 'type' => 'post' |
| | | ] |
| | | ); |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\integrations; |
| | | |
| | | use JVBase\meta\Meta; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | trait SyncToTerm |
| | | { |
| | | use SyncTo, SyncHelpers, UserConnection; |
| | | public function addSaveTerm():void |
| | | { |
| | | if (empty($this->syncTaxonomies)) { |
| | | return; |
| | | } |
| | | if (!has_action('saved_term', [$this, 'handleSaveTerm'])) { |
| | | add_action('saved_term', [$this, 'handleSaveTerm'], 20, 3); |
| | | } |
| | | } |
| | | public function removeSaveTerm():void |
| | | { |
| | | if (empty($this->syncTaxonomies)) { |
| | | return; |
| | | } |
| | | remove_action('saved_term', [$this, 'handleSaveTerm'], 20, 3); |
| | | } |
| | | protected function handleSaveTerm(int $termID, int $tt_id, string $taxonomy, bool $update, array $args):void |
| | | { |
| | | $tax = jvbNoBase($taxonomy); |
| | | if (!in_array($tax, $this->syncTaxonomies)) { |
| | | return; |
| | | } |
| | | $registrar = Registrar::getInstance($tax); |
| | | if (!$registrar) { |
| | | return; |
| | | } |
| | | |
| | | $settings = $registrar->getIntegrationConfig($this->service_name); |
| | | if (!$settings) { |
| | | return; |
| | | } |
| | | |
| | | $fields = $this->getSyncFields($termID, 'term'); |
| | | if (!$fields['share_to_'.$this->service_name]) { |
| | | return; |
| | | } |
| | | |
| | | $isShared = array_key_exists("_{$this->service_name}_item_id", $fields) && !empty($fields["_{$this->service_name}_item_id"]); |
| | | |
| | | if ($isShared && $update && !$fields['_keep_synced_'.$this->service_name]) { |
| | | return; |
| | | } |
| | | $this->removeSaveTerm(); |
| | | $this->handleTheSaveTerm($termID, $update, $taxonomy, $settings); |
| | | $this->addSaveTerm(); |
| | | } |
| | | |
| | | /** |
| | | * @param int $termID |
| | | * @param bool $update |
| | | * @param array $settings |
| | | * @return void |
| | | */ |
| | | protected function handleTheSaveTerm(int $termID, bool $update, string $taxonomy, array $settings):void |
| | | { |
| | | error_log('==== ['.$this->title.']::handleTheSaveTerm ===='); |
| | | //TODO: Figure out some sort of permissions for if the user can share this term, particularly for content types |
| | | //TODO: If this is a content type, it likely has its own integration. This is an edmonton.ink problem, so I'm offloading it for now |
| | | $this->queueOperation(self::$syncTo, [ |
| | | 'terms' => [$termID], |
| | | 'user' => get_current_user_id(), |
| | | ], [ |
| | | 'priority' => 'high', |
| | | 'delay' => 30, |
| | | ]); |
| | | Meta::forTerm($termID)->set('_'.$this->service_name.'_sync_status', 'queued'); |
| | | } |
| | | |
| | | public function addDeleteTerm():void |
| | | { |
| | | if (!has_action('pre_delete_term', [$this, 'handleDeleteTerm'])) { |
| | | add_action('pre_delete_term', [$this, 'handleDeleteTerm']); |
| | | } |
| | | } |
| | | public function removeDeleteTerm():void |
| | | { |
| | | remove_action('pre_delete_term', [$this, 'handleDeleteTerm']); |
| | | } |
| | | public function handleDeleteTerm(int $termID, string $taxonomy):void |
| | | { |
| | | if (!$this->canSync['delete']) { |
| | | return; |
| | | } |
| | | $tax = jvbNoBase($taxonomy); |
| | | if (!in_array($tax, $this->syncTaxonomies)) { |
| | | return; |
| | | } |
| | | $fields = $this->getSyncFields($termID, 'term'); |
| | | if (empty($fields["_{$this->service_name}_item_id"])) { |
| | | return; |
| | | } |
| | | JVB()->queue()->add( |
| | | self::$deleteFrom, |
| | | 0, |
| | | [ |
| | | 'fields' => [$termID => $fields], |
| | | 'service' => $this->service_name, |
| | | 'type' => 'term' |
| | | ] |
| | | ); |
| | | } |
| | | } |
| New file |
| | |
| | | <?php |
| | | namespace JVBase\integrations; |
| | | |
| | | use JVBase\meta\Meta; |
| | | use JVBase\meta\Sanitizer; |
| | | use JVBase\registrar\Registrar; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | | exit; |
| | | } |
| | | |
| | | trait SyncToUser |
| | | { |
| | | use SyncTo, SyncHelpers, UserConnection; |
| | | |
| | | public function addSaveUser():void |
| | | { |
| | | if (empty($this->syncUsers)) { |
| | | return; |
| | | } |
| | | if (!has_action('profile_update', [$this, 'handleUpdateUser'])) { |
| | | add_action('profile_update', [$this, 'handleUpdateUser'], 20, 1); |
| | | } |
| | | if (!has_action('user_register', [$this, 'handleUpdateUser'])) { |
| | | add_action('user_register', [$this, 'handleUpdateUser'], 20, 1); |
| | | } |
| | | } |
| | | public function removeSaveUser():void |
| | | { |
| | | if (empty($this->syncUsers)) { |
| | | return; |
| | | } |
| | | remove_action('profile_update', [$this, 'handleUpdateUser'], 20); |
| | | remove_action('user_register', [$this, 'handleUpdateUser'], 20); |
| | | } |
| | | protected function handleUpdateUser(int $userID):void |
| | | { |
| | | $user = $this->getOrCreateUser($userID); |
| | | if (!$user) { |
| | | return; |
| | | } |
| | | |
| | | $fields = $this->getSyncFields($userID, 'user'); |
| | | if (!empty($fields["_{$this->service_name}_item_id"])) { |
| | | return; |
| | | } |
| | | |
| | | $this->removeSaveUser(); |
| | | $this->handleTheSaveUser($userID); |
| | | $this->addSaveUser(); |
| | | } |
| | | |
| | | protected function getOrCreateUser(int $userID):string|false |
| | | { |
| | | $user = get_userdata($userID); |
| | | if (!$user || is_wp_error($user)) { |
| | | return false; |
| | | } |
| | | $role = jvbUserRole($userID); |
| | | if (!in_array(jvbNoBase($role), $this->syncUsers)) { |
| | | return false; |
| | | } |
| | | $fields = $this->getSyncFields($userID, 'user'); |
| | | if (!empty($fields["_{$this->service_name}_item_id"])) { |
| | | return $fields["_{$this->service_name}_item_id"]; |
| | | } |
| | | |
| | | $meta = Meta::forUser($userID); |
| | | $serviceUserID = $this->searchServiceForUser($user->user_email??''); |
| | | if ($serviceUserID) { |
| | | $meta->set("{$this->service_name}_item_id", $serviceUserID); |
| | | return $serviceUserID; |
| | | } |
| | | |
| | | $created = $this->createServiceUser($userID, $fields); |
| | | if ($created) { |
| | | return $created; |
| | | } |
| | | return false; |
| | | } |
| | | public function searchServiceForUser(string $email):string|false |
| | | { |
| | | if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { |
| | | return false; |
| | | } |
| | | return $this->handleEmailSearch($email); |
| | | } |
| | | |
| | | /** |
| | | * Searches for existing user from sanitized email |
| | | * @param string $email |
| | | * @return string|false The found service's User ID or false on failure |
| | | */ |
| | | abstract public function handleEmailSearch(string $email):string|false; |
| | | |
| | | public function createServiceUser(int $userID, array $fields):string|false |
| | | { |
| | | $checked = $this->validateUserFields($userID, $fields); |
| | | $response = $this->handleCreateUser($checked); |
| | | if ($response['success'] && !empty($response['data'])) { |
| | | $meta = Meta::forUser($userID); |
| | | $meta->set("{$this->service_name}_item_id", $response['data']['id']); |
| | | } |
| | | return $response['success'] ? $response['data']['id']??false : false; |
| | | } |
| | | |
| | | protected function validateUserFields(int $userID, array $fields):array|false |
| | | { |
| | | foreach ($fields as $f => $v) { |
| | | $v = match ($f) { |
| | | 'email' => filter_var($v, FILTER_SANITIZE_EMAIL), |
| | | 'phone' => Sanitizer::sanitizePhone($v), |
| | | default => sanitize_text_field($v), |
| | | }; |
| | | if (empty($v) && in_array($f, $this->requiredUserFields())) { |
| | | return false; |
| | | } |
| | | $fields[$f] = $v; |
| | | } |
| | | return $fields; |
| | | } |
| | | protected function requiredUserFields():array |
| | | { |
| | | return []; |
| | | } |
| | | |
| | | /** |
| | | * @param array $data User Data |
| | | * @return array The created user ID or false on failure |
| | | */ |
| | | abstract protected function handleCreateUser(array $data):array; |
| | | |
| | | /** |
| | | * @param int $userID |
| | | * @return void |
| | | */ |
| | | protected function handleTheSaveUser(int $userID):void |
| | | { |
| | | error_log('==== ['.$this->title.']::handleTheSaveUser ===='); |
| | | //TODO: This is likely only for stuff like customers. |
| | | //If we have multiple stores connected, we may have to queue operations to update every connection's customer account if they made an order with that store |
| | | $role = jvbUserRole($userID); |
| | | $registrar = Registrar::getInstance($role); |
| | | if (!$registrar || !$registrar->hasIntegration($this->service_name)) { |
| | | return; |
| | | } |
| | | |
| | | $this->queueOperation(self::$syncTo, [ |
| | | 'users' => [$userID], |
| | | ]); |
| | | } |
| | | |
| | | public function addDeleteUser():void |
| | | { |
| | | if (!has_action('delete_user', [$this, 'handleDeleteTerm'])) { |
| | | add_action('delete_user', [$this, 'handleDeleteTerm']); |
| | | } |
| | | } |
| | | public function removeDeleteUser():void |
| | | { |
| | | remove_action('delete_user', [$this, 'handleDeleteTerm']); |
| | | } |
| | | public function handleDeleteUser(int $userID):void |
| | | { |
| | | if (!$this->canSync['delete']) { |
| | | return; |
| | | } |
| | | |
| | | $fields = $this->getSyncFields($userID, 'user'); |
| | | if (empty($fields["_{$this->service_name}_item_id"])) { |
| | | return; |
| | | } |
| | | JVB()->queue()->add( |
| | | self::$deleteFrom, |
| | | 0, |
| | | [ |
| | | 'fields' => [$userID => $fields], |
| | | 'service' => $this->service_name, |
| | | 'type' => 'user' |
| | | ] |
| | | ); |
| | | } |
| | | } |
| | |
| | | */ |
| | | protected array $syncUsers = []; |
| | | /** |
| | | * @var array Integration's specific content types. Set by child classes' setContentTypes |
| | | * @var array Integration's specific content types. An array of [ TYPE => [$fields] ] Set by child classes' setContentTypes |
| | | */ |
| | | protected array $contentTypes = []; |
| | | |
| | |
| | | namespace JVBase\integrations\services; |
| | | use Exception; |
| | | use JVBase\integrations\Admin; |
| | | use JVBase\integrations\APIEndpoint; |
| | | use JVBase\integrations\Instance; |
| | | use JVBase\integrations\Integrations; |
| | | use JVBase\integrations\OAuth; |
| | |
| | | Webhooks; |
| | | |
| | | protected string $orderPostType = '_square_order'; |
| | | protected string $apiVersion = '2025-09-24'; |
| | | protected string $apiVersion = '2026-07-15'; |
| | | public bool $hasBatchCreate = true; |
| | | public bool $hasBatchUpdate = true; |
| | | public bool $hasBatchDelete = true; |
| | | public array $apiEndpoints = [ |
| | | |
| | | ]; |
| | | |
| | | protected function __construct() |
| | | { |
| | |
| | | |
| | | protected function initialize(): void |
| | | { |
| | | // TODO: Implement initialize() method. |
| | | $this->defineEndpoints(); |
| | | } |
| | | protected function defineEndpoints():void |
| | | { |
| | | $this->defineCustomerEndpoints(); |
| | | $this->defineProductEndpoints(); |
| | | $this->defineOrderEndpoints(); |
| | | $this->definePaymentEndpoints(); |
| | | } |
| | | protected function defineCustomerEndpoints():void |
| | | { |
| | | $customer = new APIEndpoint(); |
| | | $customer->setCreate('/v2/customers'); |
| | | $customer->setBatchCreate('/v2/customers/bulk-create'); |
| | | $customer->setDelete('/v2/customers/');//add the item_id to the end |
| | | $customer->setBatchDelete('/v2/customers/bulk-delete'); |
| | | $customer->setUpdate('/v2/customers/');//add the item_id to the end |
| | | $customer->setBatchUpdate('/v2/customers/bulk-update'); |
| | | $customer->setSearch('/v2/customers/search'); |
| | | $customer->setImport('/v2/customers'); |
| | | $this->apiEndpoints['customer'] = $customer; |
| | | } |
| | | protected function defineProductEndpoints():void |
| | | { |
| | | $product = new APIEndpoint(); |
| | | $product->setSearch('/v2/catalog/search-catalog-items'); |
| | | $product->setImport('/v2/catalog/list'); |
| | | $product->setImage('/v2/catalog/images'); |
| | | $product->setCreate('/v2/catalog/object'); |
| | | $product->setBatchCreate('/v2/catalog/batch-upsert'); |
| | | $product->setUpdate('/v2/catalog/object'); |
| | | $product->setBatchUpdate('/v2/catalog/batch-upsert'); |
| | | $product->setDelete('/v2/catalog/object/');//add the item_id to the end |
| | | $product->setBatchDelete('/v2/catalog/batch-delete'); |
| | | $this->apiEndpoints['product'] = $product; |
| | | } |
| | | |
| | | protected function defineOrderEndpoints():void |
| | | { |
| | | $order = new APIEndpoint(); |
| | | $order->setCreate('/v2/orders'); |
| | | $order->setSearch('/v2/orders/search'); |
| | | $order->setUpdate('/v2/orders/'); //add order_id |
| | | $this->apiEndpoints['order'] = $order; |
| | | } |
| | | protected function definePaymentEndpoints():void |
| | | { |
| | | $payment = new APIEndpoint(); |
| | | $payment->setCreate('/v2/payments'); |
| | | |
| | | $this->apiEndpoints['payment'] = $payment; |
| | | } |
| | | |
| | | protected function refreshAccessToken(OAuthCredentials $credentials): array |
| | |
| | | |
| | | protected function setContentTypes(): void |
| | | { |
| | | // TODO: Implement setContentTypes() method. |
| | | $base = $this->getBaseFields(); |
| | | $this->contentTypes = [ |
| | | 'customer' => $this->getCustomerFields(), |
| | | 'REGULAR' => $base, |
| | | 'FOOD_AND_BEV' => array_merge($base, $this->setFoodAndBevFields()), |
| | | 'APPOINTMENTS_SERVICE' => array_merge($base, $this->setAppointmentServiceFields()), |
| | | 'EVENT' => array_merge($base, $this->setEventFields()), |
| | | 'GIFT_CARD' => array_merge($base, $this->setGiftCardFields()), |
| | | ]; |
| | | } |
| | | protected function getCustomerFields():array |
| | | { |
| | | return [ |
| | | 'address' => [ |
| | | 'type' => 'group', |
| | | 'fields' => [ |
| | | 'address_line_1' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Address Line 1', |
| | | 'hint' => 'ex: 6551 111 St NW', |
| | | 'required' => true, |
| | | 'section' => 'address' |
| | | ], |
| | | 'address_line_2' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Address Line 2', |
| | | 'hint' => 'ex: Unit 2', |
| | | 'section' => 'address' |
| | | ], |
| | | 'locality' => [ |
| | | 'type' => 'text', |
| | | 'label'=> 'City', |
| | | 'section' => 'address', |
| | | 'required' => true, |
| | | ], |
| | | 'administrative_district_level_1' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Province', |
| | | 'hint' => 'The two-character code, example: AB', |
| | | 'default'=> 'AB', |
| | | 'section' => 'address', |
| | | 'required' => true, |
| | | ], |
| | | 'postal_code' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Postal Code', |
| | | 'section'=> 'address' |
| | | ], |
| | | 'country' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Country Code', |
| | | 'hint' => 'The tw-character country code, example: CA', |
| | | 'default' => 'CA', |
| | | 'section' => 'address', |
| | | 'required' => true, |
| | | ], |
| | | 'first_name' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'First Name (if different)', |
| | | 'hint' => 'Optional field to save the address to someone that isn\'t you' |
| | | ], |
| | | 'last_name' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Last Name (if different)', |
| | | 'hint' => 'Optional field to save the address to someone that isn\'t you' |
| | | ] |
| | | ] |
| | | ] |
| | | ]; |
| | | } |
| | | protected function getBaseFields():array |
| | | { |
| | | return [ |
| | | 'price' => [ |
| | | 'type' => 'number', |
| | | 'label' => 'Price', |
| | | 'step' => 0.01, |
| | | 'max' => 99999, |
| | | 'description' => 'Price for this variation' |
| | | ], |
| | | 'sku' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'SKU', |
| | | 'description' => 'Stock keeping unit' |
| | | ], |
| | | 'cart_quantity' => [ |
| | | 'type' => 'number', |
| | | 'label' => 'Quantity', |
| | | 'hidden' => true, |
| | | ], |
| | | 'available_for_pickup' => [ |
| | | 'type' => 'true_false', |
| | | 'label' => 'Available for Pick Up', |
| | | 'section'=> 'square-advanced' |
| | | ], |
| | | 'available_online' => [ |
| | | 'type' => 'true_false', |
| | | 'label' => 'Available online', |
| | | 'section'=> 'square-advanced' |
| | | ], |
| | | 'product_variations' => [ |
| | | 'type' => 'repeater', |
| | | 'label' => 'Product Variations', |
| | | 'description' => 'Different versions of this product (sizes, colors, etc.)', |
| | | 'add_label' => 'Add Variation', |
| | | 'section' => 'variations', |
| | | 'fields' => $this->setVariationFields() |
| | | ], |
| | | ]; |
| | | } |
| | | protected function setVariationFields(?string $type = null):array |
| | | { |
| | | $fields = [ |
| | | 'name' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Variation Name', |
| | | 'description' => 'e.g., "Small", "Large", "Red", etc.' |
| | | ], |
| | | 'sku' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'SKU', |
| | | 'description' => 'Stock keeping unit' |
| | | ], |
| | | 'price' => [ |
| | | 'type' => 'number', |
| | | 'label' => 'Price', |
| | | 'step' => 0.01, |
| | | 'max' => 99999, |
| | | 'description' => 'Price for this variation' |
| | | ], |
| | | 'track_inventory' => [ |
| | | 'type' => 'true_false', |
| | | 'label' => 'Track Inventory', |
| | | ], |
| | | 'item_id' => [ |
| | | 'type' => 'text', |
| | | 'label' => 'Square Variation ID', |
| | | 'description' => 'Square catalog ID for this variation', |
| | | 'hidden' => true |
| | | ], |
| | | 'last_sync' => [ |
| | | 'type' => 'datetime', |
| | | 'label' => 'Last Sync', |
| | | 'hidden' => true |
| | | ] |
| | | ]; |
| | | |
| | | switch ($type) { |
| | | case 'FOOD_AND_BEV': |
| | | $fields['ingredients'] = [ |
| | | 'type' => 'textarea', |
| | | 'label' => 'Ingredients List', |
| | | 'description' => 'Separate ingredients with commas', |
| | | ]; |
| | | $fields['preparation_time_duration'] = [ |
| | | 'type' => 'number', |
| | | 'label' => 'Preparation time (in minutes)', |
| | | ]; |
| | | break; |
| | | case 'GIFT_CARD': |
| | | $fields['gift_card_type'] = [ |
| | | 'type' => 'select', |
| | | 'label' => 'Gift Card Type', |
| | | 'options' => [ |
| | | 'PHYSICAL' => 'Physical', |
| | | 'DIGITAL' => 'Digital', |
| | | ], |
| | | 'default' => 'DIGITAL', |
| | | ]; |
| | | break; |
| | | case 'APPOINTMENTS_SERVICE': |
| | | $fields['service_duration'] = [ |
| | | 'type' => 'number', |
| | | 'label' => 'Duration of Service in Minutes' |
| | | ]; |
| | | $fields['available_for_booking'] = [ |
| | | 'type' => 'true_false', |
| | | 'label' => 'Available for Booking' |
| | | ]; |
| | | break; |
| | | } |
| | | return array_combine( |
| | | array_map(fn($k) => '_square_' . $k, array_keys($fields)), |
| | | $fields |
| | | ); |
| | | } |
| | | protected function setFoodAndBevFields():array |
| | | { |
| | | return [ |
| | | 'ingredients' => [ |
| | | 'type' => 'textarea', |
| | | 'label' => 'Ingredients', |
| | | 'hint' => 'A comma separated list of ingredients' |
| | | ], |
| | | 'preparation_time_duration' => [ |
| | | 'label' => 'Preparation Time', |
| | | 'type' => 'number', |
| | | 'hint' => 'Preparation time in minutes' |
| | | ], |
| | | 'product_variations' => [ |
| | | 'type' => 'repeater', |
| | | 'label' => 'Product Variations', |
| | | 'description' => 'Different versions of this product (sizes, colors, etc.)', |
| | | 'add_label' => 'Add Variation', |
| | | 'section' => 'variations', |
| | | 'fields' => $this->setVariationFields('FOOD_AND_BEV') |
| | | ], |
| | | ]; |
| | | /* TODO: Set definitions for: |
| | | 'dietary_preferences' => [ |
| | | |
| | | ], |
| | | 'calories_text' => [ |
| | | |
| | | ], |
| | | */ |
| | | } |
| | | protected function setAppointmentServiceFields():array |
| | | { |
| | | return [ |
| | | |
| | | 'product_variations' => [ |
| | | 'type' => 'repeater', |
| | | 'label' => 'Product Variations', |
| | | 'description' => 'Different versions of this product (sizes, colors, etc.)', |
| | | 'add_label' => 'Add Variation', |
| | | 'section' => 'variations', |
| | | 'fields' => $this->setVariationFields('APPOINTMENTS_SERVICE') |
| | | ], |
| | | ]; |
| | | } |
| | | |
| | | protected function setEventFields():array |
| | | { |
| | | return [ |
| | | 'venue_details' => [ |
| | | 'type' => 'textarea', |
| | | 'label' => 'Venue Details', |
| | | ], |
| | | 'capacity' => [ |
| | | 'type' => 'number', |
| | | 'label' => 'Capacity' |
| | | ], |
| | | 'product_variations' => [ |
| | | 'type' => 'repeater', |
| | | 'label' => 'Product Variations', |
| | | 'description' => 'Different versions of this product (sizes, colors, etc.)', |
| | | 'add_label' => 'Add Variation', |
| | | 'section' => 'variations', |
| | | 'fields' => $this->setVariationFields('EVENT') |
| | | ], |
| | | ]; |
| | | } |
| | | protected function setGiftCardFields():array |
| | | { |
| | | return [ |
| | | // 'allowed_locations' => [ |
| | | // |
| | | // ], |
| | | 'product_variations' => [ |
| | | 'type' => 'repeater', |
| | | 'label' => 'Product Variations', |
| | | 'description' => 'Different versions of this product (sizes, colors, etc.)', |
| | | 'add_label' => 'Add Variation', |
| | | 'section' => 'variations', |
| | | 'fields' => $this->setVariationFields('GIFT_CARD') |
| | | ], |
| | | ]; |
| | | } |
| | | |
| | | protected function formatForService(int $itemID, string $type = 'post'): array |
| | |
| | | ] |
| | | ] |
| | | ); |
| | | return $response['id']; |
| | | return $response['data']; |
| | | } catch (Exception $e) { |
| | | return false; |
| | | } |
| | | } |
| | | protected function handleEmailSearchResponse(WP_Error|array $response, string $method):array |
| | | { |
| | | |
| | | $data = empty($response['customers']) |
| | | ? false |
| | | : $response['customers'][0]??false; |
| | | return $this->response(true, 'Successfully got results.', $data); |
| | | } |
| | | |
| | | protected function handleCreateUser(array $data): array |
| | | { |
| | | try { |
| | | return $this->postRequest( |
| | | sprintf('%s/v2/customers',$this->getBaseURL()), |
| | | sprintf( |
| | | '%s%s', |
| | | $this->getBaseURL(), |
| | | $this->apiEndpoints['customer']['create']->getEndpoint() |
| | | ), |
| | | $data |
| | | ); |
| | | } catch (Exception $e) { |
| | |
| | | } |
| | | protected function handleCreateUserResponse(WP_Error|array $response, string $method):array |
| | | { |
| | | |
| | | $data = empty($response['customer']) |
| | | ? false |
| | | : $response['customer']; |
| | | return $this->response(true, 'Successfully got results.', $data); |
| | | } |
| | | protected function validateUserFields(int $userID, array $fields):array|false |
| | | { |
| | |
| | | } |
| | | } |
| | | |
| | | public function getAdditionalFields(?string $content_type = null):array |
| | | { |
| | | $this->setContentTypes(); |
| | | if (is_null($content_type) || !array_key_exists($content_type, $this->contentTypes)) { |
| | | return []; |
| | | } |
| | | return $this->contentTypes[$content_type]; |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | } |
| | |
| | | |
| | | public function getIntegrationFields():array |
| | | { |
| | | if ($this->registrar && $this->registrar->getType() === 'user' && $this->config->isCustomer()) { |
| | | return JVB()->connect($this->service_name)->getAdditionalFields('customer'); |
| | | } |
| | | $fields = [ |
| | | 'share_to_'.$this->service_name => [ |
| | | 'type' => 'true_false', |
| | |
| | | } |
| | | $additional = JVB()->connect($this->service_name)->getAdditionalFields($this->config->getContentType()); |
| | | if (!empty($additional)) { |
| | | $additional = array_combine( |
| | | array_map(fn($k) => str_starts_with($k, '_'.$this->service_name) ? $k : "_{$this->service_name}_{$k}", array_keys($additional)), |
| | | $additional |
| | | ); |
| | | $fields = array_merge($fields, $additional); |
| | | } |
| | | return $fields; |