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/SyncTo.php |  531 +++++++++++++---------------------------------------------
 1 files changed, 119 insertions(+), 412 deletions(-)

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'
-			]
-		);
-	}
 }

--
Gitblit v1.10.0