From 0dfe1d8afafc59c4a5559c498342668d5a58d6ef Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 23 Jul 2026 22:41:41 +0000
Subject: [PATCH] =Still working away at the Integrations overhaul
---
inc/integrations/APIEndpoint.php | 95 +++
inc/registrar/helpers/AddIntegrationFields.php | 7
inc/integrations/SyncToTerm.php | 115 ++++
inc/integrations/Endpoint.php | 88 +++
inc/integrations/SyncToPost.php | 140 +++++
inc/integrations/services/Square.php | 359 +++++++++++++
inc/integrations/Integrations.php | 3
inc/integrations/SyncFromTerm.php | 1
inc/integrations/EndpointField.php | 57 ++
inc/integrations/SyncFromPost.php | 1
inc/integrations/SyncToUser.php | 183 ++++++
inc/integrations/_Base.php | 2
inc/integrations/SyncHelpers.php | 14
inc/integrations/SyncFromUser.php | 1
inc/integrations/SyncTo.php | 531 ++++---------------
15 files changed, 1,169 insertions(+), 428 deletions(-)
diff --git a/inc/integrations/APIEndpoint.php b/inc/integrations/APIEndpoint.php
new file mode 100644
index 0000000..a786e52
--- /dev/null
+++ b/inc/integrations/APIEndpoint.php
@@ -0,0 +1,95 @@
+<?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;
+ }
+}
diff --git a/inc/integrations/Endpoint.php b/inc/integrations/Endpoint.php
new file mode 100644
index 0000000..e44bf74
--- /dev/null
+++ b/inc/integrations/Endpoint.php
@@ -0,0 +1,88 @@
+<?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;
+ }
+}
diff --git a/inc/integrations/EndpointField.php b/inc/integrations/EndpointField.php
new file mode 100644
index 0000000..f8660a8
--- /dev/null
+++ b/inc/integrations/EndpointField.php
@@ -0,0 +1,57 @@
+<?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;
+ }
+}
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index f9a34ef..f055894 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -29,6 +29,9 @@
* 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
diff --git a/inc/integrations/SyncFromPost.php b/inc/integrations/SyncFromPost.php
new file mode 100644
index 0000000..b3d9bbc
--- /dev/null
+++ b/inc/integrations/SyncFromPost.php
@@ -0,0 +1 @@
+<?php
diff --git a/inc/integrations/SyncFromTerm.php b/inc/integrations/SyncFromTerm.php
new file mode 100644
index 0000000..b3d9bbc
--- /dev/null
+++ b/inc/integrations/SyncFromTerm.php
@@ -0,0 +1 @@
+<?php
diff --git a/inc/integrations/SyncFromUser.php b/inc/integrations/SyncFromUser.php
new file mode 100644
index 0000000..b3d9bbc
--- /dev/null
+++ b/inc/integrations/SyncFromUser.php
@@ -0,0 +1 @@
+<?php
diff --git a/inc/integrations/SyncHelpers.php b/inc/integrations/SyncHelpers.php
index 778902a..7abc2fe 100644
--- a/inc/integrations/SyncHelpers.php
+++ b/inc/integrations/SyncHelpers.php
@@ -179,13 +179,17 @@
$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;
}
diff --git a/inc/integrations/SyncTo.php b/inc/integrations/SyncTo.php
index 223a7db..077887b 100644
--- a/inc/integrations/SyncTo.php
+++ b/inc/integrations/SyncTo.php
@@ -14,6 +14,20 @@
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;
/**
@@ -29,7 +43,7 @@
* 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
{
@@ -45,14 +59,32 @@
]);
}
}
- 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'],
@@ -97,7 +129,7 @@
} else {
$success = $errors = [];
foreach ($items as $item) {
- $itemResult = $this->createOne($item);
+ $itemResult = $this->createOneToService($item);
if (!is_wp_error($itemResult)) {
$success[] = $itemResult;
} else {
@@ -121,9 +153,9 @@
* @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');
}
/**
@@ -157,7 +189,12 @@
*****************************************************************/
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) {
@@ -211,7 +248,7 @@
$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 {
@@ -236,9 +273,9 @@
* @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');
}
/**
@@ -267,6 +304,62 @@
]
];
}
+
+ 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
*****************************************************************/
@@ -320,6 +413,22 @@
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,
@@ -332,406 +441,4 @@
];
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'
- ]
- );
- }
}
diff --git a/inc/integrations/SyncToPost.php b/inc/integrations/SyncToPost.php
new file mode 100644
index 0000000..2226604
--- /dev/null
+++ b/inc/integrations/SyncToPost.php
@@ -0,0 +1,140 @@
+<?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'
+ ]
+ );
+ }
+}
diff --git a/inc/integrations/SyncToTerm.php b/inc/integrations/SyncToTerm.php
new file mode 100644
index 0000000..0f830a6
--- /dev/null
+++ b/inc/integrations/SyncToTerm.php
@@ -0,0 +1,115 @@
+<?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'
+ ]
+ );
+ }
+}
diff --git a/inc/integrations/SyncToUser.php b/inc/integrations/SyncToUser.php
new file mode 100644
index 0000000..51290e5
--- /dev/null
+++ b/inc/integrations/SyncToUser.php
@@ -0,0 +1,183 @@
+<?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'
+ ]
+ );
+ }
+}
diff --git a/inc/integrations/_Base.php b/inc/integrations/_Base.php
index 80e71f8..1697e69 100644
--- a/inc/integrations/_Base.php
+++ b/inc/integrations/_Base.php
@@ -89,7 +89,7 @@
*/
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 = [];
diff --git a/inc/integrations/services/Square.php b/inc/integrations/services/Square.php
index e3dbfff..a2b6c9a 100644
--- a/inc/integrations/services/Square.php
+++ b/inc/integrations/services/Square.php
@@ -3,6 +3,7 @@
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;
@@ -35,7 +36,13 @@
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()
{
@@ -157,8 +164,58 @@
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
{
@@ -227,8 +284,275 @@
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
{
@@ -255,21 +579,28 @@
]
]
);
- 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) {
@@ -278,7 +609,10 @@
}
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
{
@@ -616,5 +950,16 @@
}
}
+ 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];
+ }
+
+
+
}
diff --git a/inc/registrar/helpers/AddIntegrationFields.php b/inc/registrar/helpers/AddIntegrationFields.php
index 952cd51..fd656ec 100644
--- a/inc/registrar/helpers/AddIntegrationFields.php
+++ b/inc/registrar/helpers/AddIntegrationFields.php
@@ -65,9 +65,6 @@
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',
@@ -136,6 +133,10 @@
}
$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;
--
Gitblit v1.10.0