From c68aefb847b09daa0697de7684d3451e2e68ce1e Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 09 Jul 2026 23:10:23 +0000
Subject: [PATCH] =Refactor of Integrations.php to be a bit more useful with a suite of methods for create, update, delete back and forths for integrations where sharing is enabled. Also considering a single instance with a connect method to connect as the site (no user) vs connecting as an individual user - rather than recreating instances for every user.

---
 inc/integrations/Integrations.php |  824 +++++++++++++++++++++++++++++++++++++++++++++-------------
 1 files changed, 639 insertions(+), 185 deletions(-)

diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index b1958bc..f7ce033 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -2,14 +2,22 @@
 namespace JVBase\integrations;
 
 use Exception;
-use JVBase\managers\CacheManager;
-use JVBase\managers\UploadManager;
-use JVBase\meta\MetaManager;
+use JVBase\managers\Cache;
+use JVBase\managers\queue\executors\IntegrationExecutor;
+use JVBase\managers\queue\mergers\DefaultMerger;
+use JVBase\managers\queue\TypeConfig;
+use JVBase\meta\Form;
+use JVBase\meta\Meta;
 use JVBase\managers\ErrorHandler;
+use JVBase\registrar\helpers\AddIntegrationFields;
+use JVBase\registrar\Registrar;
 use WP_Error;
 use WP_Post;
+use WP_Query;
 use WP_REST_Request;
 use WP_REST_Response;
+use WP_Term;
+use WP_User;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -26,6 +34,21 @@
  */
 abstract class Integrations
 {
+	//Flag to allow for custom settings (defaults, etc) for an integration in the dashboard
+	public static bool $hasExtraOptions = false;
+	public bool $hasBatchCreate = false;
+	public bool $hasBatchUpdate = false;
+	public bool $hasBatchDelete = false;
+	public bool $canCreateOnUpdate = false;
+	/**
+	 * Queue types
+	 * These types match with IntegrationExecutor
+	 */
+	protected static string $syncTo;
+	protected static string $deleteFrom;
+	protected static string $syncFrom;
+	protected static string $syncCustomer;
+	protected static string $import;
 	/**
 	 * API Configuration
 	 * These properties define how the integration connects to external services
@@ -55,7 +78,7 @@
 	 * Used for UI rendering in admin interfaces
 	 */
 	public string $title;  // Human-readable service name (e.g., 'Google My Business')
-	public string $icon;   // Phosphoricons icon slug
+	public string $icon = '';   // Phosphoricons icon slug
 
 	/**
 	 * Credentials & State
@@ -91,12 +114,13 @@
 		'test_connection' => 'Test Connection',
 	];
 
+	protected array $allowedContent = [];
 
 	/**
 	 * Caching Configuration
 	 */
 	protected ?string $cacheName = null;
-	protected CacheManager $cache;
+	protected Cache $cache;
 	protected array $cacheStrategy = [
 		'aggressive' => 3600,  // 1 hour for stable data (e.g., profile info)
 		'moderate' => 300,     // 5 minutes for semi-dynamic data (e.g., posts)
@@ -131,8 +155,8 @@
 		'update' => false,     // Can update already-shared posts
 		'delete' => false,     // Can remove posts from the service
 	];
-	protected array $syncPostTypes = []; // Post types that can be synced (e.g., ['artwork', 'tattoo']): usually built by querying the global JVB_CONTENT if the integration name exists as a key in [ 'integrations' => []]
-	protected array $syncTaxonomies = []; // Post types that can be synced (e.g., ['artwork', 'tattoo']): usually built by querying the global JVB_CONTENT if the integration name exists as a key in [ 'integrations' => []]
+	protected array $syncPostTypes = []; // Post types that can be synced (e.g., ['artwork', 'tattoo']): usually built by Registrar.php if the integration name exists as a key in [ 'integrations' => []]
+	protected array $syncTaxonomies = []; // Post types that can be synced (e.g., ['artwork', 'tattoo']): usually built by Registrar.php if the integration name exists as a key in [ 'integrations' => []]
 	protected array $contentTypes = [];   // Integration's available content types. Set by child classes' getContentTypes
 	protected bool $has_content = false; // Whether integration has content that can sync
 	/**
@@ -163,11 +187,11 @@
 	protected bool $is_healthy = true;
 	protected bool $handleWebhooks = false;
 
-	public function __construct(?int $userID = null)
+	protected function __construct(?int $userID = null)
 	{
 		$this->cacheName = $this->cacheName ?: $this->service_name;
 		$this->userID = $userID;
-		$this->cache = CacheManager::for('integrations_' . $this->cacheName, $this->ttl);
+		$this->cache = Cache::for('integrations_' . $this->cacheName, $this->ttl);
 
 		// Load error stats from cache
 		$this->loadErrorStats();
@@ -194,9 +218,20 @@
 			];
 		}
 
+		self::$syncTo = $this->service_name.'_sync_to';
+		self::$deleteFrom = $this->service_name.'_delete_from';
+		self::$syncFrom = $this->service_name.'_sync_from';
+		//TODO: What is the difference between sync_from and import?
+		self::$import = $this->service_name.'import_from';
+
 		add_filter('jvbShouldRenderMeta', [$this, 'checkRenderField'], 10, 4);
 	}
 
+	protected function addFilters():bool
+	{
+		return is_null($this->userID);
+	}
+
 	protected function setContentTypes():void
 	{
 
@@ -291,23 +326,7 @@
 
 	protected function getPostTypes(): void
 	{
-		$key = BASE . $this->service_name . '_sync_post_types';
-		$postTypes = get_option($key, false);
-		if (!$postTypes) {
-			$postTypes = [];
-
-			// Get from JVB_CONTENT
-
-			foreach (JVB_CONTENT as $type => $config) {
-				if (array_key_exists('integrations', $config) && array_key_exists($this->service_name, $config['integrations'])) {
-					$postTypes[] = $type;
-				}
-			}
-
-			update_option($key, $postTypes);
-		}
-
-		$this->syncPostTypes = $postTypes;
+		$this->syncPostTypes = Registrar::withIntegration($this->service_name);
 	}
 
 	protected function getTaxonomies():void
@@ -318,11 +337,10 @@
 		if (!$taxonomies) {
 			// Combine both content and taxonomy filtering
 			$taxonomies = [];
-			// Get from JVB_TAXONOMY (content taxonomies)
-			foreach (JVB_TAXONOMY as $type => $config) {
-				if (jvbCheck('is_content', $config) &&
-					isset($config['integrations'][$this->service_name])) {
-					$taxonomies[] = $type;
+			foreach (Registrar::withFeature('is_content', 'term') as $type) {
+				$registrar = Registrar::getInstance($type);
+				if ($registrar->hasIntegration($this->service_name)) {
+					$taxonomies[] = $registrar->getSlug();
 				}
 			}
 
@@ -430,7 +448,6 @@
 		} else {
 			$result =  $this->$method();
 		}
-		error_log('Action result: '.print_r($result, true));
 		if (is_wp_error($result)) {
 			return [
 				'success' => false,
@@ -651,9 +668,7 @@
 		}
 
 		try {
-			error_log('Credentials to save: '.print_r($this->credentials, true));
 			if ($this->isOAuthService && !$this->hasOAuthCredentials()){
-				error_log('Just saving credentials, we don\'t have OAuth setup yet...');
 				//If this is an OAuth service, we might only be saving the app credentials first
 				$result = true;
 			} else {
@@ -672,9 +687,9 @@
 
 	protected function clearCache():array
 	{
-		$success = $this->cache->clear();
+		$this->cache->flush();
 		return [
-			'success'	=> $success,
+			'success'	=> true,
 		];
 	}
 
@@ -762,10 +777,7 @@
 	): array|WP_Error
 	{
 		if (!$this->is_healthy) {
-			$this->logDebug('Skipping request - integration is unhealthy', [
-				'consecutive_errors' => $this->error_stats['consecutive_errors'],
-				'last_success' => $this->error_stats['last_success']
-			]);
+			return new WP_Error('unhealthy', 'Connection marked unhealthy. Skipping fetch');
 		}
 		$this->ensureInitialized();
 		if (!$this->isSetUp()){
@@ -786,15 +798,15 @@
 
 		while ($attempt < $this->maxRetries) {
 			try {
-				$this->logDebug('[Integrations] Making request to '.$this->service_name);
+//				$this->logDebug('[Integrations] Making request to '.$this->service_name);
 				$result = $this->executeRequest($method, $endpoint, $data, $baseKey, $options);
 				if (!$result) {
 					return new WP_Error('Request Error');
 				}
 				$this->recordRequest($method, $endpoint);
-				$this->logDebug('[Integrations]', [
-					'response' => $result
-				]);
+//				$this->logDebug('[Integrations]', [
+//					'response' => $result
+//				]);
 				// Reset error stats on success
 				$this->resetErrorStats();
 				return $result;
@@ -843,7 +855,7 @@
 			return null;
 		}
 
-		$this->logDebug("$method request to: $url: ".print_r($args, true));
+//		$this->logDebug("$method request to: $url: ".print_r($args, true));
 
 		// Make the request
 		$response = match($method) {
@@ -874,9 +886,9 @@
 			if ($retry_count === 0) {
 				$retry_count++;
 
-				$this->logDebug('Got 401, attempting token refresh...');
+//				$this->logDebug('Got 401, attempting token refresh...');
 				if ($this->refreshOAuthToken()) {
-					$this->logDebug('Token refreshed successfully, retrying request...');
+//					$this->logDebug('Token refreshed successfully, retrying request...');
 
 					// Rebuild request args with new token
 					$args = $this->buildRequestArgs($method, $data, $options);
@@ -932,7 +944,8 @@
 		if (!$force && $ttl > 0) {
 			$cached = $this->cache->get($cacheKey);
 			if ($cached !== false) {
-				$this->logDebug("Cache hit for: $cacheKey");
+
+//				$this->logDebug("Cache hit for: $cacheKey");
 				return $cached;
 			}
 		}
@@ -1045,11 +1058,8 @@
 	):bool {
 		$queue = JVB()->queue();
 
-		// Add service prefix to operation type
-		$operation_type = strtolower($this->service_name) . '_' . $type;
-
 		$queued =  $queue->queueOperation(
-			$operation_type,
+			$type,
 			$this->userID ?? 0,
 			array_merge($data, ['service' => $this->service_name, 'user' => $this->userID??0]),
 			array_merge([
@@ -1191,31 +1201,85 @@
 	 */
 	protected function registerHooks(): void
 	{
-		add_filter(BASE . 'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
-		add_action( 'wp_ajax_'.BASE.$this->service_name.'_oauth_callback', [$this, 'handleAjaxResponse'] );
-		add_action( 'wp_ajax_nopriv_'.BASE.$this->service_name.'_oauth_callback', [$this, 'handleAjaxResponse'] );
-		if ($this->handleWebhooks){
+		//Handled by IntegrationExecutor now
+//		add_filter(BASE . 'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
+		if (is_null($this->userID)) {
+			add_action( 'wp_ajax_'.BASE.$this->service_name.'_oauth_callback', [$this, 'handleAjaxResponse'] );
+			add_action( 'wp_ajax_nopriv_'.BASE.$this->service_name.'_oauth_callback', [$this, 'handleAjaxResponse'] );
+		}
+
+		if ($this->handleWebhooks && is_null($this->userID)){
 			$this->registerWebhookEndpoint();
 		}
 		// Let child classes register additional hooks if needed
 		$this->registerAdditionalHooks();
 
-		if (!empty($this->syncPostTypes)) {
-			add_action('save_post', [$this, 'handleSavePost'], 20, 3);
+		if (!empty($this->syncPostTypes) && is_null($this->userID)) {
+			$this->addSavePost();
 			add_action('transition_post_status', [$this, 'handlePostStatusTransition'], 10, 3);
 
 			if ($this->canSync['delete']) {
 				add_action('before_delete_post', [$this, 'handleDeletePost'], 10, 1);
 			}
 		}
-		if (!empty($this->syncTaxonomies)) {
+		if (!empty($this->syncTaxonomies) && is_null($this->userID)) {
 			add_action('saved_term', [$this, 'handleSaveTerm'], 20, 5);
 			if ($this->canSync['delete']) {
 				add_action('pre_delete_term', [$this, 'handleDeleteTerm'], 10, 2);
 			}
 		}
-	}
 
+		add_action('init', [$this, 'registerQueueTypes'], 10);
+	}
+		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 registerQueueTypes():void
+		{
+			if (empty($this->syncTaxonomies) && empty($this->syncPostTypes)) {
+				return;
+			}
+			$queue = JVB()->queue();
+			$executor = new IntegrationExecutor();
+
+			$queue->registry()->register(self::$syncTo, new TypeConfig(
+				mergeable: new DefaultMerger('items'),
+				executor: $executor,
+				chunkKey: 'items',
+				chunkSize: 50,
+				maxRetries: 3,
+			));
+
+			if ($this->canSync['delete']) {
+				$queue->registry()->register(self::$deleteFrom, new TypeConfig(
+					mergeable: new DefaultMerger('external_ids'),
+					executor: $executor,
+					chunkKey: 'external_ids',
+					chunkSize: 200,
+					maxRetries: 2
+				));
+			}
+
+			$queue->registry()->register(self::$syncFrom, new TypeConfig(
+				executor: $executor,
+				maxRetries: 3
+			));
+
+			$this->registerAdditionalQueueTypes($executor);
+		}
+			protected function registerAdditionalQueueTypes(IntegrationExecutor $executor):void
+			{
+				//Empty. Integration extensions can register additional operation types from here.
+			}
 	public function handleAjaxResponse()
 	{
 
@@ -1390,7 +1454,7 @@
 					// Only attempt refresh once per request
 					if (!$this->token_refresh_attempted) {
 						$this->token_refresh_attempted = true;
-						$this->logDebug('OAuth token expired, attempting refresh');
+//						$this->logDebug('OAuth token expired, attempting refresh');
 
 						if (!$this->refreshOAuthToken()) {
 							$this->logError('Failed to refresh expired OAuth token - stopping execution');
@@ -1399,14 +1463,14 @@
 						}
 					} else {
 						// Already attempted refresh in this request
-						$this->logDebug('Token refresh already attempted, skipping');
+//						$this->logDebug('Token refresh already attempted, skipping');
 						return;
 					}
 				}
 				// Check if we should proactively refresh (before expiry)
 				elseif ($this->shouldRefreshToken() && !$this->token_refresh_attempted) {
 					$this->token_refresh_attempted = true;
-					$this->logDebug('OAuth token should be refreshed proactively');
+//					$this->logDebug('OAuth token should be refreshed proactively');
 					if (!$this->refreshOAuthToken()) {
 						$this->logError('Failed to proactively refresh OAuth token');
 						// Not critical - token is still valid, so continue
@@ -1418,51 +1482,6 @@
 	}
 
 
-	/**
-	 * Switch user context
-	 */
-	public function switchUser(int $user_id): void
-	{
-		if ($this->userID === $user_id) {
-			return;
-		}
-
-		// Clean up current context
-		$this->cleanup();
-
-		// Switch context
-		$this->userID = $user_id;
-		$this->credentials = [];
-		$this->resetTokenRefreshFlag();  // ADD THIS LINE
-
-		$this->ensureInitialized();
-	}
-
-	public function getAsUser(int $user_id) {
-		return new $this($user_id);
-	}
-
-	/**
-	 * Clean up resources
-	 */
-	protected function cleanup(): void
-	{
-		// Clear sensitive data
-		$this->credentials = [];
-
-		// Clear request history
-		$this->request_history = [];
-	}
-
-	/**
-	 * Destructor - ensure cleanup
-	 */
-	public function __destruct()
-	{
-		$this->cleanup();
-	}
-
-
 	/***************************************************************
 		ERROR HANDLING
 	 ***************************************************************/
@@ -1631,10 +1650,7 @@
 			$params = $this->addOAuthParams($params);
 		}
 
-		$auth_url = $this->oauth['authorize'] . '?' . http_build_query($params);
-
-
-		return $auth_url;
+		return $this->oauth['authorize'] . '?' . http_build_query($params);
 	}
 
 	/**
@@ -2089,20 +2105,11 @@
 	 */
 	protected function mapFieldsToService(int $postID, array $mapping): array
 	{
-		$meta_manager = new MetaManager($postID, 'post');
-		$post = get_post($postID);
+		$meta_manager = Meta::forPost($postID);
 		$service_data = [];
 
 		foreach ($mapping as $wp_field => $service_field) {
-			$value = null;
-
-			// Check if it's a post field
-			if (property_exists($post, $wp_field)) {
-				$value = $post->$wp_field;
-			} else {
-				// It's a meta field
-				$value = $meta_manager->getValue($wp_field);
-			}
+			$value = $meta_manager->get($wp_field);
 
 			if ($value !== null && $value !== '') {
 				$this->setNestedValue($service_data, $service_field, $value);
@@ -2141,52 +2148,69 @@
 	 */
 	public function handleSavePost(int $postID, WP_Post $post, bool $update): void
 	{
-		error_log('Testing For Save Post');
-		if (jvbNoSaveIt($postID, $post)) {
-			error_log('Excluded by jvbNoSaveIt');
+		if (!is_null($this->userID)) {
 			return;
 		}
-		if (empty($this->syncPostTypes)) {
-			error_log('No Syncable post types');
+		error_log('=== ['.$this->service_name.']::handleSavePost called');
+
+		if (!$postID || $postID === 0) {
+			return;
+		}
+		$postType = jvbNoBase($post->post_type);
+
+		if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
+		if (wp_is_post_revision($postID)) return;
+
+
+		if (empty($this->syncPostTypes) || !in_array(jvbNoBase($postType), $this->syncPostTypes)) {
+			error_log('Not handling save for '.$this->service_name.' because there are no syncPostTypes: '.print_r($this->syncPostTypes, true));
 			return;
 		}
 
-		$config = JVB_CONTENT[jvbNoBase($post->post_type)]??null;
-		if (!$config) {
-			error_log('No Config set');
+		$registrar = Registrar::getInstance($postType);
+		if (!$registrar){
 			return;
 		}
 
-		$settings = $config['integrations'][$this->service_name]??null;
+		$settings = $registrar->hasIntegration($this->service_name)??null;
 		if (!$settings) {
-			error_log('No settings');
+			error_log('Not handling save for '.$this->service_name.' because of no registrar settings');
 			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', ['schedule_'.$this->service_name]);
-		error_log('Fields to check: '.print_r($fields, true));
 		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 = isset($fields["_{$this->service_name}_item_id"]);
 		if ($update && $isShared && !$fields['_keep_synced_'.$this->service_name]) {
-			error_log('Do not keep synced, not syncing with '.$this->service_name);
+			error_log('Not handling save for '.$this->service_name.' because it is already shared, and not set to keep synced. ');
 			return;
 		}
 
 		if ($post->post_status !== 'publish' && !$isShared) {
-			error_log('Not published and not already shared');
+			error_log('Not handling save for '.$this->service_name.' because post status is not publish, and it is not already shared.');
 			return;
 		}
-		error_log('Sending to integration for processing...');
+		error_log('==== Sending to integration\'s handleTheSavePost '.$this->service_name.' ====');
+		$this->removeSavePost();
 		$this->handleTheSavePost($postID, $post, $update, $settings);
+		$this->addSavePost();
 	}
 
 
 	protected function getSyncFields(int $postID, string $type, array $additional = []):array
 	{
-		$meta = new MetaManager($postID, $type);
+		$meta = new Meta($postID, $type);
 		$fieldsToCheck = [
 			'share_to_' . $this->service_name,
 			'_keep_synced_' . $this->service_name,
@@ -2269,13 +2293,13 @@
 		if (!in_array($noBase, $this->syncTaxonomies)) {
 			return;
 		}
-		// Check if taxonomy is content-type
-		$config = JVB_TAXONOMY[$noBase] ?? null;
-		if (!$config || !($config['is_content'] ?? false)) {
+		$registrar = Registrar::getInstance($noBase);
+		if (!$registrar->hasFeature('is_content')) {
 			return;
 		}
 
-		$settings = $config['integrations'][$this->service_name] ?? null;
+
+		$settings = $registrar->getIntegrationConfig($this->service_name);
 		if (!$settings) {
 			return;
 		}
@@ -2652,11 +2676,6 @@
 		];
 		$this->is_healthy = true;
 		$this->saveErrorStats();
-
-		$this->logDebug('Integration health manually reset', [
-			'reset_by' => get_current_user_id(),
-			'reset_time' => time()
-		]);
 	}
 
 	/**
@@ -2725,6 +2744,20 @@
 		return $this->title;
 	}
 
+	public static function title():string
+	{
+		return static::getInstance()->getTitle();
+	}
+	public static function icon():string
+	{
+		return static::getInstance()->getIcon();
+	}
+
+	public static function hasExtraOptions():bool
+	{
+		return static::getInstance()::$hasExtraOptions;
+	}
+
 	/*********************************************************************
 		RENDERING
 	 *********************************************************************/
@@ -2791,9 +2824,6 @@
 
 		// Check for duplicate processing (idempotency)
 		if ($this->isWebhookProcessed($payload)) {
-			$this->logDebug('Webhook already processed', [
-				'webhook_id' => $this->extractWebhookId($payload)
-			]);
 			return true; // Return true to prevent retries
 		}
 
@@ -2955,7 +2985,7 @@
 			return '';
 		}
 
-		$meta = new MetaManager($this->userID, 'integrations');
+		$meta = Meta::forOptions($this->userID.'_integrations');
 		$is_connected = $this->isSetUp();
 		$credentials = $this->getCredentials();
 
@@ -2968,7 +2998,7 @@
 		?>
 		<form id="<?=$this->service_name?>" class="integration <?php echo $is_connected ? 'connected' : 'disconnected'; ?>"
 			 data-service="<?php echo esc_attr($this->service_name); ?>">
-			<div class="header row btw">
+			<div class="header row x-btw">
 				<h3><?php echo esc_html($this->title); ?></h3>
 				<div class="setup">
 					<?php if ($is_connected): ?>
@@ -3032,7 +3062,7 @@
 						$config['value'] = $credentials[$name]??'';
 						$config['autocomplete'] = 'off';
 						$config['base'] = $this->service_name.'_';
-						$meta->render('form', $name, $config);
+						echo Form::render($name, '', $config);
 					}
 				}
 				if ($this->handleWebhooks) {
@@ -3063,7 +3093,7 @@
 							$config['value'] = $credentials[$name]??'';
 							$config['base'] = $this->service_name.'_';
 							$config['autocomplete'] = 'off';
-							$meta->render('form', $name, $config);
+							Form::render($name,null, $config);
 						}
 						?>
 					</details>
@@ -3079,7 +3109,7 @@
 				}
 				?>
 			</div>
-			<div class="actions row btw wrap">
+			<div class="actions row x-btw wrap">
 				<?php
 				foreach ($this->buttons as $action => $label) {
 					if (!$is_connected && $action !== 'save_credentials') {
@@ -3206,7 +3236,6 @@
 		// Verify nonce
 		$nonce_field = 'jvb_integration_nonce_' . $service;
 		$nonce_action = 'jvb_integration_save_' . $service;
-		error_log('handleAdminPost: '.print_r($_POST, true));
 		if (!isset($_POST[$nonce_field]) || !wp_verify_nonce($_POST[$nonce_field], $nonce_action)) {
 			wp_die('Security check failed');
 		}
@@ -3303,7 +3332,7 @@
 		if (empty($types)) {
 			return;
 		}
-		$meta = new MetaManager($this->userID, 'integrations');
+		$meta = Meta::forOptions($this->userID.'_integrations');
 		?>
 		<form>
 			<h1><?= $this->title?> Defaults:</h1>
@@ -3315,16 +3344,19 @@
 
 				$config['base'] = $this->service_name.'_';
 				$config['autocomplete'] = 'off';
-				$meta->render('form', $name, $config);
+				echo Form::render($name, null, $config);
 			}
 			foreach ($this->syncPostTypes as $type) {
-				$config = JVB_CONTENT[$type];
+				$registrar = Registrar::getInstance($type);
+
+				$icon = $registrar->getIcon();
+				$icon = $icon === '' ? jvbDefaultIcon() : $icon;
 				?>
 				<details>
-					<summary><?= jvbIcon($config['icon']) ?><?= $config['singular']?> Defaults</summary>
+					<summary><?= jvbIcon($icon) ?><?= $registrar->getSingular()?> Defaults</summary>
 					<?php
-					$fields = new \JVBase\registry\providers\IntegrationFieldProvider();
-					$fields = $fields->getIntegrationFields($this->service_name, $config);
+					$fields = new AddIntegrationFields($this->service_name);
+					$fields = $fields->getIntegrationFields();
 					foreach($fields as $name=>$c) {
 						$c['required'] = false;
 						if ($c['type'] === 'number') {
@@ -3335,7 +3367,7 @@
 							$c['hint'] = $c['description'];
 							unset($c['description']);
 						}
-						$meta->render('form', $name, $c);
+						echo Form::render($name, null, $c);
 					}
 					?>
 				</details>
@@ -3361,28 +3393,10 @@
 			return [];
 		}
 
-		$key = BASE.$this->service_name.'_enabled_content_types';
-		$enabled = get_option($key);
-		if (!$enabled) {
-			$enabled = [];
-			foreach (array_merge(JVB_CONTENT, JVB_TAXONOMY) as $content => $config) {
-				if (!array_key_exists('integrations', $config)) {
-					continue;
-				}
-				if (!array_key_exists($this->service_name, $config['integrations'])) {
-					continue;
-				}
-				if (!array_key_exists('content_type', $config['integrations'][$this->service_name])) {
-					continue;
-				}
-				$type = $config['integrations'][$this->service_name]['content_type'];
-				if (!in_array($type, $enabled)) {
-					$enabled[] = $type;
-				}
-			}
-			update_option($key, $enabled);
-		}
-		return $enabled;
+		return array_filter(array_map(function($registrar) {
+			$registrar = Registrar::getInstance($registrar);
+			return $registrar->getIntegration($this->service_name)->getContentType();
+		}, Registrar::withIntegration($this->service_name)));
 	}
 
 	protected function getSupportedImage(int $imgID):int
@@ -3551,4 +3565,444 @@
 	{
 		$this->token_refresh_attempted = false;
 	}
+
+	public function getAllowedContent():array
+	{
+		return $this->allowedContent;
+	}
+
+	/**
+	 * Used by JVBase\registrar\helpers\AddIntegrationFields.php
+	 * @return array
+	 */
+	public function getAdditionalFields(?string $content_type = null):array
+	{
+		return [];
+	}
+
+	public function getIcon():string
+	{
+		return $this->icon;
+	}
+
+	public function getServiceItemID(int $itemID, string $type = 'post'):string
+	{
+		return match($type) {
+			'post'	=> Meta::forPost($itemID)->get("_{$this->service_name}_item_id"),
+			'term','taxonomy'	=> Meta::forTerm($itemID)->get("_{$this->service_name}_item_id"),
+			'user'	=> Meta::forUser($itemID)->get("_{$this->service_name}_item_id"),
+			default => ''
+		};
+	}
+
+	public function canBatchUpdate():bool
+	{
+		return $this->hasBatchUpdate;
+	}
+	public function canBatchCreate():bool
+	{
+		return $this->hasBatchCreate;
+	}
+	public function canBatchDelete():bool
+	{
+		return $this->hasBatchDelete;
+	}
+
+	protected function updateItemStatus(int|array $items, string $status = 'pending', string $type = 'post'):void
+	{
+		if (!is_array($items)) {
+			$items = [$items];
+		}
+		foreach ($items as $ID) {
+			match ($type) {
+				'post'	=> Meta::forPost($ID)->set('_'.$this->service_name.'_sync_status', $status),
+				'term'	=> Meta::forTerm($ID)->set('_'.$this->service_name.'_sync_status', $status),
+				'user'	=> Meta::forUser($ID)->set('_'.$this->service_name.'_sync_status', $status),
+				default => false
+			};
+		}
+	}
+
+	public function updateBatchToService(array $data):array
+	{
+		$type = $data['type']??'post';
+
+		$newlyCreated = [];
+		if (!$this->canCreateOnUpdate) {
+			$created = array_filter($data['items'], function($ID) use ($type) {
+				return !empty($this->getServiceItemID($ID, $type));
+			});
+
+			//Test to see if we have any that haven't been created yet.
+			//For some services, items may have to be created before they can be updated
+			if (count($created) !== count($data['items'])) {
+				$notCreated = array_filter($data['items'], function($ID) use ($type) {
+					return empty($this->getServiceItemID($ID, $type));
+				});
+				$newData = $data;
+				$newData['items'] = $notCreated;
+				$newlyCreated = $this->createBatchToService($newData);
+			}
+
+			// If we don't have any that are created, just send the noUpdatedItems response, with any newly created items added
+			if (empty($created)) {
+				return $this->noUpdatedItems($newlyCreated);
+			}
+			$data['items'] = $created;
+		}
+
+
+		$items = $this->formatItems($data['items'], $type);
+
+		if (empty($items)) {
+			return $this->noUpdatedItems($newlyCreated);
+		}
+
+		if ($this->hasBatchUpdate) {
+			$response = $this->sendBatchUpdate($items);
+			if (!is_wp_error($response)) {
+				$result = $this->processBatchUpdateResponse($data, $response);
+			} else {
+				$this->logError('Batch update failed',[
+					'method'	=> 'updateBatchToService',
+					'item_ids'	=> $data['items'],
+					'error'		=> $response
+				]);
+				$this->updateItemStatus($data['items'], 'error');
+
+				$result = [
+					'outcome'	=> 'failed_permanent',
+					'result'	=> 'Could not update items'
+				];
+			}
+		} else {
+			$success = $errors = [];
+			//Does not have batch update, manually update each one
+			foreach ($items as $item) {
+				$itemResult = $this->updateOne($item);
+				if (!is_wp_error($itemResult)) {
+					$success[] = $itemResult;
+				} else {
+					$errors[] = $itemResult;
+				}
+			}
+
+			$result = [
+				'outcome'	=> empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial'),
+				'result'	=> [
+					'success'	=> $success,
+					'errors'	=> $errors
+				]
+			];
+		}
+
+		return array_merge($result, $newlyCreated);
+	}
+
+	/**
+	 * To be implemented by integration extension. Updates a single item
+	 * @param array $item
+	 * @return array|WP_Error
+	 */
+		protected function updateOne(array $item):array|WP_Error
+		{
+			return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own updateOne method');
+		}
+
+	/**
+	 * Overridden by child classes
+	 * @param array $items
+	 * @return array|WP_Error
+	 */
+		protected function sendBatchUpdate(array $items):array|WP_Error
+		{
+			return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own sendBatchUpdate');
+		}
+	/**
+	 * Overridden by child classes
+	 * @param array $items
+	 * @return array|WP_Error
+	 */
+		protected function sendBatchCreate(array $items):array|WP_Error
+		{
+			return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own sendBatchCreate');
+		}
+
+	/**
+	 * To be implemented by extensions
+	 * @param array $data
+	 * @param array $response
+	 * @return array
+	 */
+		protected function processBatchUpdateResponse(array $data, array $response):array
+		{
+			return [
+				'outcome'	=> 'failed',
+				'result'	=> [
+					'message'	=> $this->service_name.' should implement processBatchUpdateResponse.'
+				]
+			];
+		}
+	/**
+	 * To be implemented by extensions
+	 * @param array $data
+	 * @param array $response
+	 * @return array
+	 */
+		protected function processBatchCreateResponse(array $data, array $response):array
+		{
+			return [
+				'outcome'	=> 'failed',
+				'result'	=> [
+					'message'	=> $this->service_name.' should implement processBatchCreateResponse.'
+				]
+			];
+		}
+
+	/**
+	 * Defaults to an array of formatted items. Can be overridden in child classes
+	 * @param array $itemIDs
+	 * @param string $type
+	 * @return array
+	 */
+		protected function formatItems(array $itemIDs, string $type):array
+		{
+			$items = [];
+			foreach ($itemIDs as $ID) {
+				try {
+					$items[] = $this->formatForService($ID, $type);
+				} catch (Exception $e){
+					$this->logError('Could not format Item for service',[
+						'itemID' => $ID,
+						'type'	=> $type,
+						'message'	=> $e->getMessage(),
+					]);
+				}
+			}
+			return $items;
+		}
+
+	/**
+	 * Must be defined according to how each service needs it to be
+	 * @param int $itemID
+	 * @param string $type
+	 * @return array
+	 * @throws Exception
+	 */
+		protected function formatForService(int $itemID, string $type = 'post'):array
+		{
+			throw new Exception('formatForService must be implemented by child class');
+		}
+		protected function noUpdatedItems(array $created):array
+		{
+			$result =  [
+				'outcome'	=> 'success',
+				'result'	=> [
+					'message'	=> 'No items to update',
+					'updated'	=> [],
+					'errors'	=> [],
+				]
+			];
+
+			if (!empty($created)) {
+				error_log('Result before: '.print_r($result, true));
+				$result = array_merge($result, $created);
+				error_log('Result after merge: '.print_r($result, true));
+			}
+			return $result;
+		}
+
+		protected function noCreatedItems(array $updated):array
+		{
+			$result = [
+				'outcome'	=> 'success',
+				'result'	=> [
+					'message'	=> 'No items to create',
+					'created'	=> [],
+					'errors'	=> [],
+				]
+			];
+			if (!empty($updated)) {
+				$result = array_merge($result, $updated);
+			}
+			return $result;
+		}
+
+		public function createBatchToService($data):array
+		{
+			$type = $data['type']??'post';
+
+			//Check if any of the submitted ids are already created
+			$created = array_filter($data['items'],
+				function($ID) use ($type) {
+					return !empty($this->getServiceItemID($ID, $type));
+				});
+
+			$updated = [];
+			if (!empty($created)) {
+				$updateData = $data;
+				$updateData['items'] = $created;
+				$updated = $this->updateBatchToService($data);
+
+				//remove any updated items from the original items to process
+				$data['items'] = array_filter($data['items'], function ($ID) use ($created) {
+					return !in_array($ID, $created);
+				});
+			}
+
+			$items = $this->formatItems($data['items'], $type);
+			if (empty($items)) {
+				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',[
+						'method'	=> 'createBatchToService',
+						'item_ids'	=> $data['items'],
+						'error'		=> $response
+					]);
+					$this->updateItemStatus($data['items'], 'error');
+
+					$result = [
+						'outcome'	=> 'failed_permanent',
+						'result'	=> 'Could not update items'
+					];
+				}
+			} else {
+				$success = $errors = [];
+				foreach ($items as $item) {
+					$itemResult = $this->createOne($item);
+					if (!is_wp_error($itemResult)) {
+						$success[] = $itemResult;
+					} else {
+						$errors[] = $itemResult;
+					}
+				}
+				$result = [
+					'outcome'	=> empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial'),
+					'result'	=> [
+						'success'	=> $success,
+						'errors'	=> $errors
+					]
+				];
+			}
+
+			return array_merge($result, $updated);
+		}
+
+		/**
+		 * To be implemented by integration extension. Updates a single item
+		 * @param array $item
+		 * @return array|WP_Error
+		 */
+		protected function createOne(array $item):array|WP_Error
+		{
+			return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own createOne');
+		}
+
+	public function findWPItem(string $integrationID, ?string $type = null):array|false
+	{
+		if (empty($integrationID)) {
+			error_log('Integrations::findWPItem No Integration ID received.');
+			return false;
+		}
+		$types = ['post', 'term', 'user'];
+		if (!is_null($type) && !in_array($type, $types)) {
+			error_log('Integrations::findWPItem invalid type sent: '.$type.'; defaulting to checking all');
+			$type = null;
+		}
+		if (!is_null($type)) {
+			return $this->queryWPType($integrationID, $type);
+		} else {
+			foreach ($types as $type) {
+				$check = $this->queryWPType($integrationID, $type);
+				if ($check) {
+					return $check;
+				}
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * @param string $integrationID
+	 * @param string|null $type
+	 * @return array|false if success, an array of $ID and $type
+	 */
+		public function queryWPType(string $integrationID, ?string $type = null):array|false
+		{
+			if (!in_array($type, ['post', 'term', 'user'])) {
+				return $this->findWPItem($integrationID);
+			}
+			return match($type) {
+				'post'	=> $this->findWPPost($integrationID),
+				'term'	=> $this->findWPTerm($integrationID),
+				'user'	=> $this->findWPUser($integrationID),
+				default => false
+			};
+		}
+		public function findWPPost(string $integrationID):array|false
+		{
+			if (empty($integrationID)) {
+				return false;
+			}
+			$get = new WP_Query([
+				'meta_query'	=> [
+					'key'	=> BASE."_{$this->service_name}_item_id",
+					'value'	=> $integrationID,
+				],
+				'fields'	=> 'ids',
+				'posts_per_page' => 1,
+			]);
+			return $get->have_posts() && !empty($get->posts) ? [$get->posts[0], 'post'] : false;
+		}
+		public function findWPTerm(string $integrationID):array|false
+		{
+			if (empty($integrationID)) {
+				return false;
+			}
+			$get = get_terms([
+				'meta_query'	=> [
+					'key'	=> BASE."_{$this->service_name}_item_id",
+					'value'	=> $integrationID,
+				],
+				'fields'	=> 'ids',
+				'hide_empty'=> true,
+				'number'	=> 1
+			]);
+			return is_wp_error($get) || empty($get) ? false : [$get[0], 'term'];
+		}
+		public function findWPUser(string $integrationID):array|false
+		{
+			if (empty($integrationID)) {
+				return false;
+			}
+			$get = get_users([
+				'meta_query'	=> [
+					'key'	=> BASE."_{$this->service_name}_item_id",
+					'value'	=> $integrationID,
+				],
+				'fields'	=> 'ids',
+				'number'	=> 1,
+			]);
+			return is_wp_error($get) || empty($get) ? false : [$get[0], 'user'];
+		}
+
+	/*********************************************************************************
+	 * Syncing WP items from Service
+	*********************************************************************************/
+	protected function createItemFromService(array $itemData):bool
+	{
+
+	}
 }

--
Gitblit v1.10.0