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