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 |  182 +++++++++++++++++++++++++++++++++++++++------
 1 files changed, 158 insertions(+), 24 deletions(-)

diff --git a/inc/integrations/SyncTo.php b/inc/integrations/SyncTo.php
index dd1e9ca..077887b 100644
--- a/inc/integrations/SyncTo.php
+++ b/inc/integrations/SyncTo.php
@@ -2,14 +2,34 @@
 namespace JVBase\integrations;
 
 use Exception;
+use JVBase\meta\Meta;
+use JVBase\meta\Sanitizer;
+use JVBase\registrar\Registrar;
 use WP_Error;
+use WP_Post;
+use WP_Term;
+use WP_User;
 
 if (!defined('ABSPATH')) {
 	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;
+	use SyncHelpers, UserConnection;
 	/**
 	 * Must be defined according to how each service needs it to be
 	 * @param int $itemID
@@ -17,16 +37,13 @@
 	 * @return array
 	 * @throws Exception
 	 */
-	protected function formatForService(int $itemID, string $type = 'post'):array
-	{
-		throw new Exception('formatForService must be implemented by child class');
-	}
+	abstract protected function formatForService(int $itemID, string $type = 'post'):array;
 
 	/**
 	 * 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
 	{
@@ -35,21 +52,39 @@
 			try {
 				$items[] = $this->formatForService($ID, $type);
 			} catch (Exception $e){
-				$this->logError('Could not format Item for service',[
+				$this->logError('formatItems', 'Could not format Item for service',[
 					'itemID' => $ID,
 					'type'	=> $type,
 					'message'	=> $e->getMessage(),
 				]);
 			}
 		}
-		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'],
@@ -74,18 +109,12 @@
 			return $this->noCreatedItems($updated);
 		}
 
-		$response = [
-			'outcome'	=> 'failed',
-			'result'	=> [
-				'message'	=> 'No result'
-			]
-		];
 		if ($this->hasBatchCreate) {
 			$response = $this->sendBatchCreate($items);
 			if (!is_wp_error($response)) {
 				$result = $this->processBatchCreateResponse($data, $response);
 			} else {
-				$this->logError('Batch create failed',[
+				$this->logError('createBatchToService','Batch create failed',[
 					'method'	=> 'createBatchToService',
 					'item_ids'	=> $data['items'],
 					'error'		=> $response
@@ -100,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 {
@@ -124,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');
 	}
 
 	/**
@@ -160,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) {
@@ -198,7 +232,7 @@
 			if (!is_wp_error($response)) {
 				$result = $this->processBatchUpdateResponse($data, $response);
 			} else {
-				$this->logError('Batch update failed',[
+				$this->logError('updateBatchToService','Batch update failed',[
 					'method'	=> 'updateBatchToService',
 					'item_ids'	=> $data['items'],
 					'error'		=> $response
@@ -214,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 {
@@ -239,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');
 	}
 
 	/**
@@ -270,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
 	*****************************************************************/
@@ -307,4 +397,48 @@
 		}
 		return $result;
 	}
+
+	/****************************************************************
+	 * UTILITY
+	****************************************************************/
+	protected function getSyncFields(int $itemID, string $type, array $additionalFields = []):array
+	{
+		$meta = match ($type) {
+			'post'	=> Meta::forPost($itemID),
+			'term'	=> Meta::forTerm($itemID),
+			'user'	=> Meta::forUser($itemID),
+			default => false
+		};
+		if (!$meta) {
+			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,
+			"_{$this->service_name}_item_id",
+			"_{$this->service_name}_last_sync",
+			"_{$this->service_name}_shared_at",
+			"_{$this->service_name}_sync_status",
+			"_{$this->service_name}_scheduled_at",
+			... $additionalFields
+		];
+		return $meta->getAll($fields);
+	}
 }

--
Gitblit v1.10.0