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/registrar/helpers/AddIntegrationFields.php       |    7 
 JVBase.php                                           |   27 
 inc/managers/queue/Processor.php                     |   22 
 inc/integrations/Square.php                          |  267 +++----------
 base/Site.php                                        |    3 
 inc/integrations/Integrations.php                    |  567 +++++++++++++++++++++++++++--
 inc/integrations/Facebook.php                        |   35 -
 inc/managers/queue/executors/IntegrationExecutor.php |   25 
 inc/integrations/Instagram.php                       |   23 
 inc/registrar/config/Integration.php                 |    9 
 inc/helpers/all.php                                  |    1 
 assets/js/concise/FormController.js                  |    2 
 assets/js/concise/BioManager.js                      |    2 
 assets/js/concise/AuthManager.js                     |    6 
 inc/integrations/Helcim.php                          |   63 +-
 assets/js/concise/CRUDOld.js                         |    2 
 inc/integrations/GoogleMyBusiness.php                |   38 -
 inc/registrar/Registrar.php                          |    6 
 18 files changed, 707 insertions(+), 398 deletions(-)

diff --git a/JVBase.php b/JVBase.php
index 90f903d..e771147 100644
--- a/JVBase.php
+++ b/JVBase.php
@@ -86,7 +86,7 @@
 	}
 
 
-	public function __construct()
+	private function __construct()
 	{
 		$this->customBlocks = new CustomBlocks();
 		$this->managers = [
@@ -296,17 +296,20 @@
 
 	public function connect(string $service, ?int $userID = null): mixed
 	{
-		if ($userID) {
-			if (!$this->userCanConnect($service, $userID)) {
-				return null;
-			}
-
-			if (!array_key_exists($service, $this->integrations)) {
-				return null;
-			}
-			return $this->serviceMap[$service]::getInstance($userID);
-		}
-		return (array_key_exists($service, $this->integrations)) ? $this->integrations[$service] : null;
+//		if ($userID) {
+//			if (!$this->userCanConnect($service, $userID)) {
+//				return null;
+//			}
+//
+//			if (!array_key_exists($service, $this->integrations)) {
+//				return null;
+//			}
+//
+//			return $this->serviceMap[$service]::getInstance($userID);
+//		}
+		return is_null($userID) ?
+			$this->integrations[$service]??null :
+			(array_key_exists($service, $this->integrations) ? $this->serviceMap[$service]::getInstance($userID) : null);
 	}
 
 	public function userCanConnect(string $service, int $userID): bool
diff --git a/assets/js/concise/AuthManager.js b/assets/js/concise/AuthManager.js
index 29946d2..b8f5c36 100644
--- a/assets/js/concise/AuthManager.js
+++ b/assets/js/concise/AuthManager.js
@@ -78,9 +78,9 @@
 				'X-WP-Nonce': this.getNonce()
 			};
 
-			if (!isFormData && typeof options.body === 'object' && !Object.hasOwn(options.body, 'user')) {
-				options.body.user = this.getUser();
-			}
+			// if (!isFormData && typeof options.body === 'object' && !Object.hasOwn(options.body, 'user')) {
+			// 	options.body.user = this.getUser();
+			// }
 
 			let obj = {
 				...options,
diff --git a/assets/js/concise/BioManager.js b/assets/js/concise/BioManager.js
index cd058a2..bf2f63a 100644
--- a/assets/js/concise/BioManager.js
+++ b/assets/js/concise/BioManager.js
@@ -19,7 +19,7 @@
 		if (data === null) {
 			return;
 		}
-        data.user = window.auth.getUser();
+        // data.user = window.auth.getUser();
 
 		if (Object.hasOwn(data, 'term_name') && data['term_name'] === ''){
 			delete data['term_name'];
diff --git a/assets/js/concise/CRUDOld.js b/assets/js/concise/CRUDOld.js
index 998483d..f662f0c 100644
--- a/assets/js/concise/CRUDOld.js
+++ b/assets/js/concise/CRUDOld.js
@@ -508,7 +508,7 @@
 
 		// Get the base content type
 		data.content = form.closest('dialog')?.dataset.content || this.content;
-		data.user = window.auth.getUser();
+		// data.user = window.auth.getUser();
 
 		// Collect timeline entries
 		const timelineGroups = form.querySelectorAll('.upload-group');
diff --git a/assets/js/concise/FormController.js b/assets/js/concise/FormController.js
index ccbea31..e5f3868 100644
--- a/assets/js/concise/FormController.js
+++ b/assets/js/concise/FormController.js
@@ -669,7 +669,7 @@
 		this.cancelServerSave(form.id);
 
 		if (form.useQueue && window.jvbQueue) {
-			changes.user = window.auth.getUser();
+			// changes.user = window.auth.getUser();
 			window.jvbQueue.addToQueue({
 				endpoint: form.options.endpoint,
 				data: changes,
diff --git a/base/Site.php b/base/Site.php
index 49df15e..f6ecad0 100644
--- a/base/Site.php
+++ b/base/Site.php
@@ -130,7 +130,8 @@
 
 	public static function has(string $property):bool
 	{
-		return property_exists(self::class, $property) && self::${$property} === true;
+		$inst = self::getInstance();
+		return property_exists($inst, $property) && $inst::${$property} === true;
 	}
 
 	public static function hasAny(array $properties):bool
diff --git a/inc/helpers/all.php b/inc/helpers/all.php
index 274e00d..63eac34 100644
--- a/inc/helpers/all.php
+++ b/inc/helpers/all.php
@@ -88,4 +88,3 @@
 		'post_type'		=> $type
 	]);
 }
-
diff --git a/inc/integrations/Facebook.php b/inc/integrations/Facebook.php
index f30ee8f..2812607 100644
--- a/inc/integrations/Facebook.php
+++ b/inc/integrations/Facebook.php
@@ -360,31 +360,18 @@
 	 */
 	protected function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings): void
 	{
-		$post_type = $settings['fb_type'] ?? 'post';
-		$sync_immediately = $settings['immediate'] ?? false;
 
-		// Determine the Facebook post type
-		$fb_endpoint = self::FB_POST_TYPES[$post_type] ?? 'feed';
-
-		$operation_data = [
-			'post_id' => $postID,
-			'fb_type' => $post_type,
-			'endpoint' => $fb_endpoint,
-			'page_id' => $this->page_id
-		];
-
-		// Check for scheduled posting
-		$schedule_time = get_post_meta($postID, BASE . 'schedule_facebook', true);
-		$options = [];
-
-		if ($schedule_time && strtotime($schedule_time) > time()) {
-			$options['scheduled'] = strtotime($schedule_time);
-		} elseif (!$sync_immediately) {
-			$options['delay'] = 300; // 5 minute delay for batching
+		if (!$this->hasOAuthCredentials()) {
+			error_log('OAuth Not set up for '.$this->service_name);
+			return;
 		}
-
-		$operation = $update ? 'update_post' : 'create_post';
-		$this->queueOperation($operation, $operation_data, $options);
+		$this->queueOperation(self::$syncTo, [
+			'items'		=> [$postID],
+			'user'		=> user_can($post->post_author, 'manage_options') ? null : $post->post_author
+		], [
+			'delay'	=> 60
+		]);
+		Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued');
 	}
 
 	/**
@@ -393,7 +380,7 @@
 	public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array
 	{
 		try {
-			$fb = (array_key_exists('user', $data)) ? new self((int)$data['user']) : $this;
+			$fb = (array_key_exists('user', $data) && $data['user'] !== 0) ? $this->getInstance((int)$data['user']) : $this;
 			switch ($operation->type) {
 				case 'facebook_create_post':
 					return $fb->createFacebookPost($data);
diff --git a/inc/integrations/GoogleMyBusiness.php b/inc/integrations/GoogleMyBusiness.php
index b802142..a66a81a 100644
--- a/inc/integrations/GoogleMyBusiness.php
+++ b/inc/integrations/GoogleMyBusiness.php
@@ -216,39 +216,25 @@
 
 	public function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings):void
 	{
-		$type = $settings['content_type']??'post'; //can be 'post', 'offer', 'event', 'hours', 'info',
-		$initial = $settings['initial']?? false;
-		$syncOnUpdate = $settings['update']??false;
-		if ($update && !$syncOnUpdate) {
-			return;
-		}
-		if (!$initial) {
+
+		if (!$this->hasOAuthCredentials()) {
+			error_log('OAuth Not set up for '.$this->service_name);
 			return;
 		}
 
-		$options = $data = [];
-		switch ($type) {
-			case 'menu_item':
-				$options = ['delay' => 60];
-				break;
-		}
-		if (!user_can($post->post_author, 'manage_options')) {
-			$data['user'] = $post->post_author;
-		}
-		$data['post_id'] = $postID;
-		$operation = ($update) ? 'update_' : 'create_';
-
-		$this->queueOperation(
-			$operation.$type,
-			$data,
-			$options
-		);
+		$this->queueOperation(self::$syncTo, [
+			'items'		=> [$postID],
+			'user'		=> user_can($post->post_author, 'manage_options') ? null : $post->post_author
+		], [
+			'delay'	=> 60
+		]);
+		Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued');
 	}
 
 	public function processOperation(\WP_Error|array $result, object $operation, array $data):\WP_Error|array
 	{
 		try {
-			$gmb = (array_key_exists('user', $data)) ? new self((int)$data['user']) : $this;
+			$gmb = (array_key_exists('user', $data) && $data['user'] !== 0) ? $this->getInstance((int)$data['user']) : $this;
 			$gmb->ensureInitialized();
 			switch ($operation->type) {
 				case 'gmb_create_menu_item':
@@ -481,7 +467,7 @@
 				];
 			}
 
-			$gmb = new self($data['userID']??null);
+			$gmb = self::getInstance($data['userID']??null);
 
 			// Build the complete menu structure
 			$menu_data = $gmb->collectMenu($menu_items);
diff --git a/inc/integrations/Helcim.php b/inc/integrations/Helcim.php
index 7f2556d..f5dc8a5 100644
--- a/inc/integrations/Helcim.php
+++ b/inc/integrations/Helcim.php
@@ -2,6 +2,7 @@
 namespace JVBase\integrations;
 
 use Exception;
+use JVBase\meta\Meta;
 use WP_Error;
 use WP_Post;
 use JVBase\ui\Checkout;
@@ -25,6 +26,7 @@
  */
 class Helcim extends Integrations
 {
+	protected static string $syncCustomer = 'helcim_sync_customer';
 	protected string $service_name = 'helcim';
 	protected string|array $apiBase = 'https://api.helcim.com/v2';
 
@@ -711,9 +713,6 @@
 			return $desc;
 		}, 10, 2);
 
-		// Register queue operation types with IntegrationExecutor
-		$this->registerQueueTypes();
-
 		// Register webhook endpoint (handled by parent)
 		$this->registerWebhookEndpoint();
 	}
@@ -760,38 +759,16 @@
 	}
 
 
-	protected function registerQueueTypes(): void
+	protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void
 	{
 		$queue    = JVB()->queue();
-		$executor = new IntegrationExecutor();
 
-		$queue->registry()->register('helcim_sync_to', new TypeConfig(
-			executor:  $executor,
-			chunkKey:  'items',
-			chunkSize: 10,
-			maxRetries: 3
-		));
-
-		$queue->registry()->register('helcim_sync_from', new TypeConfig(
-			executor:  $executor,
-			chunkKey:  'items',
-			chunkSize: 10,
-			maxRetries: 3
-		));
-
-		$queue->registry()->register('helcim_delete_from', new TypeConfig(
-			executor:  $executor,
-			chunkKey:  'external_ids',
-			chunkSize: 20,
-			maxRetries: 2
-		));
-
-		$queue->registry()->register('helcim_import', new TypeConfig(
+		$queue->registry()->register(self::$import, new TypeConfig(
 			executor:   $executor,
 			maxRetries: 3
 		));
 
-		$queue->registry()->register('helcim_sync_customer', new TypeConfig(
+		$queue->registry()->register(self::$syncCustomer, new TypeConfig(
 			executor:   $executor,
 			maxRetries: 2
 		));
@@ -1028,22 +1005,34 @@
 
 	protected function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void
 	{
-		$fields = $this->getSyncFields($postID, 'post', ['share_to_helcim', 'schedule_helcim']);
 
-		if (empty($fields['share_to_helcim'])) {
-			return;
-		}
-
-		// Uses IntegrationExecutor via TypeRegistry instead of FilteredExecutor
-		$this->queueOperation('sync_to', [
+		error_log('==== [Helcim]::handleTheSavePost ====');
+		$this->queueOperation(self::$syncTo, [
 			'items'   => [$postID],
-			'user_id' => $this->userID,
+			'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');
+	}
+	/**
+	 * Handle post deletion
+	 */
+	public function handleDeletePost(int $postID): void
+	{
+		$item_id = Meta::forPost($postID)->get("_{$this->service_name}_item_id");
 
-		update_post_meta($postID, BASE . '_helcim_sync_status', 'queued');
+		if (empty($item_id)) {
+			return;
+		}
+		$this->queueOperation(self::$deleteFrom, [
+			'external_ids' => [$item_id],
+			'post_id'      => $postID,
+		], [
+			'priority' => 'high',
+		]);
+
 	}
 
 	protected function handleImportFromHelcim(): array
diff --git a/inc/integrations/Instagram.php b/inc/integrations/Instagram.php
index 77abcf0..3968e45 100644
--- a/inc/integrations/Instagram.php
+++ b/inc/integrations/Instagram.php
@@ -6,6 +6,7 @@
 namespace JVBase\integrations;
 
 use JVBase\integrations\Integrations;
+use JVBase\meta\Meta;
 use WP_Error;
 use WP_REST_Request;
 use WP_REST_Response;
@@ -261,24 +262,22 @@
 	 */
 	public function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void
 	{
-		// Check if post has featured image (Instagram requirement)
+
+		if (!$this->hasOAuthCredentials()) {
+			error_log('OAuth Not set up for '.$this->service_name);
+			return;
+		}
 		if (!has_post_thumbnail($postID)) {
-			$this->logError('Cannot post to Instagram without featured image', [
-				'post_id' => $postID
-			]);
 			return;
 		}
 
-		// Queue the Instagram post creation
-		$this->queueOperation('create_post', [
-			'post_id' => $postID,
-			'type' => get_post_type($postID)
+		$this->queueOperation(self::$syncTo, [
+			'items'		=> [$postID],
+			'user'		=> user_can($post->post_author, 'manage_options') ? null : $post->post_author
 		], [
-			'priority' => 'normal',
-			'delay' => 60 // Wait 1 minute to ensure all metadata is saved
+			'delay'	=> 60
 		]);
-
-		update_post_meta($postID, BASE . '_instagram_sync_status', 'queued');
+		Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued');
 	}
 
 	/**
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index 761e6a0..f7ce033 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -3,6 +3,9 @@
 
 use Exception;
 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;
@@ -10,8 +13,11 @@
 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;
@@ -30,15 +36,19 @@
 {
 	//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 = 'sync_to';
-	protected static string $deleteFrom = 'delete_from';
-	protected static string $syncFrom = 'sync_from';
-	protected static string $syncCustomer = 'sync_customer';
-	protected static string $import = 'import';
+	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
@@ -208,9 +218,13 @@
 			];
 		}
 
+		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
@@ -312,21 +326,7 @@
 
 	protected function getPostTypes(): void
 	{
-		$key = BASE . $this->service_name . '_sync_post_types';
-		$postTypes = get_option($key, false);
-		if (!$postTypes) {
-			$postTypes = [];
-			foreach (Registrar::getRegistered('post') as $registrar) {
-				$registrar = Registrar::getInstance($registrar);
-				if ($registrar->hasIntegration($this->service_name)) {
-					$postTypes[] = $registrar->getSlug();
-				}
-			}
-
-			update_option($key, $postTypes);
-		}
-
-		$this->syncPostTypes = $postTypes;
+		$this->syncPostTypes = Registrar::withIntegration($this->service_name);
 	}
 
 	protected function getTaxonomies():void
@@ -687,9 +687,9 @@
 
 	protected function clearCache():array
 	{
-		$success = $this->cache->flush();
+		$this->cache->flush();
 		return [
-			'success'	=> $success,
+			'success'	=> true,
 		];
 	}
 
@@ -1058,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([
@@ -1204,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()
 	{
 
@@ -2097,6 +2148,11 @@
 	 */
 	public function handleSavePost(int $postID, WP_Post $post, bool $update): void
 	{
+		if (!is_null($this->userID)) {
+			return;
+		}
+		error_log('=== ['.$this->service_name.']::handleSavePost called');
+
 		if (!$postID || $postID === 0) {
 			return;
 		}
@@ -2105,40 +2161,50 @@
 		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;
+		}
+
 		$registrar = Registrar::getInstance($postType);
 		if (!$registrar){
 			return;
 		}
 
-		if (empty($this->syncPostTypes)) {
-			return;
-		}
-
 		$settings = $registrar->hasIntegration($this->service_name)??null;
 		if (!$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]);
 		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('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 handling save for '.$this->service_name.' because post status is not publish, and it is not already shared.');
 			return;
 		}
+		error_log('==== Sending to integration\'s handleTheSavePost '.$this->service_name.' ====');
+		$this->removeSavePost();
 		$this->handleTheSavePost($postID, $post, $update, $settings);
+		$this->addSavePost();
 	}
 
 
@@ -3518,4 +3584,425 @@
 	{
 		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
+	{
+
+	}
 }
diff --git a/inc/integrations/Square.php b/inc/integrations/Square.php
index 5e1a831..c0191a3 100644
--- a/inc/integrations/Square.php
+++ b/inc/integrations/Square.php
@@ -28,6 +28,7 @@
  */
 class Square extends Integrations
 {
+	protected static string $syncCustomer = 'square_sync_customer';
 	protected array $allowedContent = [
 		'REGULAR',
 		'FOOD_AND_BEV',
@@ -90,14 +91,16 @@
 	public static function getInstance(?int $userID = null):self
 	{
 		$key = is_null($userID) ? 'base' : $userID;
+
 		if (!array_key_exists($key, self::$instances)) {
-			self::$instances[$key] = new static($userID);
+			self::$instances[$key] = new self($userID);
 		}
 		return self::$instances[$key];
 	}
 
 	protected function __construct(?int $userID = null)
 	{
+
 		// Display properties
 		$this->title = 'Square';
 		$this->icon = 'square-logo';
@@ -786,8 +789,8 @@
 						'name' => $variation['name'],
 						'pricing_type' => 'FIXED_PRICING',
 						'price_money' => [
-							'amount' => intval($variation['_square_price'] * 100), // Convert to cents
-							'currency' => 'USD'
+							'amount' => $this->formatPrice($variation['_square_price']??0),
+							'currency' => $this->getCurrency()
 						]
 					]
 				];
@@ -801,8 +804,8 @@
 					'name' => 'Regular',
 					'pricing_type' => 'FIXED_PRICING',
 					'price_money' => [
-						'amount' => intval(($itemData['_square_price'] ?? 0) * 100),
-						'currency' => 'USD'
+						'amount' => $this->formatPrice($itemData['_square_price'] ?? 0),
+						'currency' => $this->getCurrency()
 					]
 				]
 			];
@@ -858,7 +861,6 @@
 			return;
 		}
 
-		add_action('wp_login', [$this, 'trackUserLogin'], 10, 2);
         add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
 
 		add_filter('jvbAdditionalActions', [Checkout::class, 'render']);
@@ -880,8 +882,6 @@
 		add_filter('jvb_checkout_browse_text', function () {
 			return 'browse our menu';
 		});
-		// Register queue executor types
-		add_action('init', [$this, 'registerQueueTypes'], 10);
 	}
 
 	/**
@@ -910,29 +910,9 @@
 			]);
 	}
 
-	public function registerQueueTypes(): void
+	protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void
 	{
 		$queue    = JVB()->queue();
-		$executor = new IntegrationExecutor();
-
-		$queue->registry()->register(self::$syncTo, new TypeConfig(
-			executor:   $executor,
-			chunkKey:   'items',
-			chunkSize:  50,
-			maxRetries: 3
-		));
-
-		$queue->registry()->register(self::$deleteFrom, new TypeConfig(
-			executor:   $executor,
-			chunkKey:   'external_ids',
-			chunkSize:  200,
-			maxRetries: 2
-		));
-
-		$queue->registry()->register(self::$syncFrom, new TypeConfig(
-			executor:   $executor,
-			maxRetries: 3
-		));
 
 		$queue->registry()->register(self::$syncCustomer, new TypeConfig(
 			executor:   $executor,
@@ -954,15 +934,15 @@
 	 */
 	protected function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void
 	{
+		error_log('==== [Square]::handleTheSavePost ====');
 		$this->queueOperation(self::$syncTo, [
 			'items'   => [$postID],
-			'user_id' => $this->userID,
+			'user'		=> user_can($post->post_author, 'manage_options') ? null : $post->post_author
 		], [
 			'priority' => 'high',
 			'delay'    => 30,
 		]);
-
-		update_post_meta($postID, BASE . '_square_sync_status', 'queued');
+		Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued');
 	}
 
 	/**
@@ -970,16 +950,18 @@
 	 */
 	public function handleDeletePost(int $postID): void
 	{
-		$square_id = get_post_meta($postID, BASE . '_square_catalog_id', true);
+		$item_id = $this->getServiceItemID($postID);
 
-		if ($square_id) {
-			$this->queueOperation(self::$deleteFrom, [
-				'external_ids' => [$square_id],
-				'post_id'      => $postID,
-			], [
-				'priority' => 'high',
-			]);
+		if (empty($item_id)) {
+			return;
 		}
+		$this->queueOperation(self::$deleteFrom, [
+			'external_ids' => [$item_id],
+			'post_id'      => $postID,
+		], [
+			'priority' => 'high',
+		]);
+
 	}
 
 	/**
@@ -1063,6 +1045,9 @@
 
 	private function processBatchSyncResponse(array $response, array $map, array &$success, array &$errors):void
 	{
+		error_log('==== SQUARE::processBatchSyncResponse =====');
+		error_log('Full response: '.print_r($response, true));
+
 		// Handle successful objects
 		if (!empty($response['objects'])) {
 			foreach ($response['objects'] as $object) {
@@ -1162,6 +1147,14 @@
 
 		// Get existing Square catalog ID if it exists
 		$existing_square_id = get_post_meta($postID, BASE . '_square_catalog_id', true);
+		$registrar = Registrar::getInstance($post_type);
+		$product_type = 'FOOD_AND_BEV';
+		if ($registrar) {
+			$conf = $registrar->getIntegration($this->service_name);
+			if ($conf) {
+				$product_type = $conf->getContentType();
+			}
+		}
 
 		// Build the base catalog object
 		$catalog_object = [
@@ -1170,7 +1163,7 @@
 			'item_data' => [
 				'name' => $post->post_title,
 				'description' => wp_strip_all_tags($post->post_content),
-				'product_type' => 'FOOD_AND_BEV',
+				'product_type' => $product_type,
 				'variations' => [],
 				'is_taxable' => true,
 			]
@@ -1182,10 +1175,9 @@
 		}
 
 		// Add variations
-		$variations = $meta->get('product_variations');
+		$variations = $meta->get('_square_product_variations');
 		if (empty($variations)) {
 			// Create default variation if none exist
-			$price = floatval($meta->get('price') ?: 0);
 			$catalog_object['item_data']['variations'][] = [
 				'type' => 'ITEM_VARIATION',
 				'id' => $existing_square_id ? null : '#'.BASE.'menu_item_' . $postID . '_var_default',
@@ -1194,32 +1186,41 @@
 					'ordinal' => 0,
 					'pricing_type' => 'FIXED_PRICING',
 					'price_money' => [
-						'amount' => intval($price * 100), // Convert dollars to cents
-						'currency' => 'CAD'
+						'amount' => $this->formatPrice($meta->get('_square_price')),
+						'currency' => $this->getCurrency()
 					],
 					'sellable' => true,
 					'stockable' => true
 				]
 			];
 		} else {
+			$resetVariations = false;
 			foreach ($variations as $index => $variation) {
-				$existing_var_id = get_post_meta($postID, BASE . '_square_variation_' . $index . '_id', true);
+				$id = '#'.BASE.'menu_item_' . $postID . '_var_' . $index;
+				if (empty($variation['item_id'])) {
+					$resetVariations = true;
+					$variations[$index]['item_id'] = $id;
+					$variation['item_id'] = $id;
+				}
 				$catalog_object['item_data']['variations'][] = [
 					'type' => 'ITEM_VARIATION',
-					'id' => $existing_var_id ?: '#'.BASE.'menu_item_' . $postID . '_var_' . $index,
+					'id' => $variation['item_id'],
 					'item_variation_data' => [
 						'name' => $variation['name'] ?? 'Variation ' . ($index + 1),
 						'ordinal' => $index,
 						'pricing_type' => 'FIXED_PRICING',
 						'price_money' => [
-							'amount' => intval(floatval($variation['price'] ?? 0) * 100),
-							'currency' => 'CAD'
+							'amount' =>$this->formatPrice($variation['price'] ?? 0),
+							'currency' => $this->getCurrency()
 						],
 						'sellable' => true,
 						'stockable' => true
 					]
 				];
 			}
+			if ($resetVariations) {
+				$meta->set('_square_product_variations', $variations);
+			}
 		}
 
 		// Add categories if they exist
@@ -1262,98 +1263,7 @@
 		return $catalog_object;
 	}
 
-	/**
-	 * Build variations from repeater field
-	 */
-	private function buildVariations(int $postID, array $values, string $post_type): array
-	{
-		$variations = [];
-		$product_variations = $values['product_variations'] ?? [];
 
-		// Get variation field mapping
-		$variation_map = $this->getVariationMapping($post_type);
-
-		// If we have repeater variations
-		if (!empty($product_variations) && is_array($product_variations)) {
-			foreach ($product_variations as $index => $variation_data) {
-				// Skip empty variations
-				if (empty($variation_data['name']) && empty($variation_data['_square_sku'])) {
-					continue;
-				}
-
-				$variation = [
-					'type' => 'ITEM_VARIATION',
-					'id' => '#' . $post_type . '_' . $postID . '_var_' . $index,
-					'item_variation_data' => [
-						'name' => $variation_data['name'] ?? 'Variation ' . ($index + 1),
-						'ordinal' => $index,
-						'pricing_type' => 'FIXED_PRICING'
-					]
-				];
-
-				// Check for existing Square variation ID
-				$square_var_id = get_post_meta($postID, BASE . '_square_variation_' . $index . '_id', true);
-				if ($square_var_id) {
-					$variation['id'] = $square_var_id;
-				}
-
-				// Map variation fields
-				foreach ($variation_map as $square_field => $wp_field) {
-					if (isset($variation_data[$wp_field])) {
-						switch ($square_field) {
-							case 'price':
-								$variation['item_variation_data']['price_money'] = [
-									'amount' => intval($variation_data[$wp_field] * 100), // Convert to cents
-									'currency' => 'CAD'
-								];
-								break;
-
-							case 'sku':
-								$variation['item_variation_data']['_square_sku'] = $variation_data[$wp_field];
-								break;
-
-							case 'track_inventory':
-								$variation['item_variation_data']['track_inventory'] = (bool)$variation_data[$wp_field];
-								break;
-
-							case 'service_duration':
-								if (!empty($variation_data[$wp_field])) {
-									$variation['item_variation_data']['service_data'] = [
-										'duration_minutes' => intval($variation_data[$wp_field])
-									];
-								}
-								break;
-
-							default:
-								$variation['item_variation_data'][$square_field] = $variation_data[$wp_field];
-								break;
-						}
-					}
-				}
-
-				$variations[] = $variation;
-			}
-		}
-
-		// If no variations exist, create a default one from base price
-		if (empty($variations) && !empty($values['price'])) {
-			$variations[] = [
-				'type' => 'ITEM_VARIATION',
-				'id' => '#' . $post_type . '_' . $postID . '_var_default',
-				'item_variation_data' => [
-					'name' => 'Regular',
-					'ordinal' => 0,
-					'pricing_type' => 'FIXED_PRICING',
-					'price_money' => [
-						'amount' => intval($values['price'] * 100),
-						'currency' => 'CAD'
-					]
-				]
-			];
-		}
-
-		return $variations;
-	}
 
 	/**
 	 * Get variation mapping for post type
@@ -1616,13 +1526,9 @@
 			];
 		}
 
-		// Generate username from email
-		$username = sanitize_user(current(explode('@', $email)));
-		$username = $this->generateUniqueUsername($username);
-
 		// Create user account without password (they'll set it via email)
 		$user_id = wp_create_user(
-			$username,
+			$email,
 			wp_generate_password(20, true, true), // Temporary random password
 			$email
 		);
@@ -1637,7 +1543,7 @@
 
 		// Set user role (assuming you have a customer role defined)
 		$user = new \WP_User($user_id);
-		$user->set_role(BASE.'foodie'); // Or whatever role
+		$user->set_role(BASE.'foodie');
 
 		// Generate password reset key
 		$reset_key = get_password_reset_key($user);
@@ -1653,7 +1559,7 @@
 		// Link to Square customer if exists
 		$square_customer_id = $this->getOrCreateSquareCustomer([
 			'email' => $email,
-			'name' => $username
+			'name' => $email
 		]);
 
 		if ($square_customer_id) {
@@ -1668,22 +1574,6 @@
 	}
 
 	/**
-	 * Generate unique username
-	 */
-	private function generateUniqueUsername(string $base): string
-	{
-		$username = $base;
-		$counter = 1;
-
-		while (username_exists($username)) {
-			$username = $base . $counter;
-			$counter++;
-		}
-
-		return $username;
-	}
-
-	/**
 	 * Send welcome email with password setup
 	 */
 	private function sendWelcomeEmail(\WP_User $user, string $reset_key): void
@@ -1699,7 +1589,7 @@
 			"%s\n\n" .
 			"Once you've set your password, you can:\n" .
 			"- View your order history\n" .
-			"- Save your favorite items\n" .
+//			"- Save your favorite items\n" .
 			"- Speed up checkout with saved payment methods\n\n" .
 			"If you didn't create this account, please ignore this email.\n\n" .
 			"Thanks,\n",
@@ -1715,49 +1605,6 @@
 		);
 	}
 
-	/**
-	 * Track user login for security
-	 */
-	public function trackUserLogin(string $user_login, \WP_User $user): void
-	{
-		// Check if user has Square integration
-		$role = jvbUserRole($user->ID);
-		$registrar = Registrar::getInstance($role);
-		if ($registrar) {
-			$config = $registrar->getIntegration($this->service_name);
-			if ($config->isCustomer()) {
-				$login_count = (int)get_user_meta($user->ID, BASE . '_square_login_count', true);
-				$login_count++;
-
-				update_user_meta($user->ID, BASE . '_square_login_count', $login_count);
-				update_user_meta($user->ID, BASE . '_square_last_login', current_time('mysql'));
-
-				// Check if password reset is needed
-				if ($login_count % self::PASSWORD_RESET_INTERVAL === 0) {
-					$this->schedulePasswordReset($user->ID);
-				}
-			}
-		}
-	}
-
-	/**
-	 * Schedule password reset for security
-	 */
-	private function schedulePasswordReset(int $user_id): void
-	{
-		update_user_meta($user_id, BASE . '_square_password_reset_required', true);
-
-		// Send notification
-		$user = get_user_by('ID', $user_id);
-		if ($user) {
-			JVB()->email()->sendEmail(
-				$user->user_email,
-				'['.get_bloginfo('name').'] Security Code',
-				'For your security, enter this code to continue accessing your account and saved payment methods.',
-			);
-		}
-	}
-
 	/******************************************************************
 	 * WEBHOOK HANDLING
 	 ******************************************************************/
@@ -3643,4 +3490,10 @@
 		}, $posts->posts);
 
 	}
+
+	public function formatPrice(string|float|int $priceValue):int
+	{
+		//Convert dollars to cents
+		return intval(floatval($priceValue) * 100);
+	}
 }
diff --git a/inc/managers/queue/Processor.php b/inc/managers/queue/Processor.php
index e735921..4178df4 100644
--- a/inc/managers/queue/Processor.php
+++ b/inc/managers/queue/Processor.php
@@ -218,17 +218,17 @@
 
 		$op->errorMessage = $e->getMessage();
 
-		JVB()->error()->log(
-			'[Queue]:processOne',
-			$e->getMessage(),
-			[
-				'operation_id' => $op->id,
-				'type'         => $op->type,
-				'user_id'      => $op->userId,
-				'retries'      => $op->retries,
-			],
-			$op->outcome === 'failed_permanent' ? 'critical' : 'warning'
-		);
+//		JVB()->error()->log(
+//			'[Queue]:processOne',
+//			$e->getMessage(),
+//			[
+//				'operation_id' => $op->id,
+//				'type'         => $op->type,
+//				'user_id'      => $op->userId,
+//				'retries'      => $op->retries,
+//			],
+//			$op->outcome === 'failed_permanent' ? 'critical' : 'warning'
+//		);
 	}
 
 	private function calculateBackoff(int $attempt): string
diff --git a/inc/managers/queue/executors/IntegrationExecutor.php b/inc/managers/queue/executors/IntegrationExecutor.php
index ea79960..428dc88 100644
--- a/inc/managers/queue/executors/IntegrationExecutor.php
+++ b/inc/managers/queue/executors/IntegrationExecutor.php
@@ -86,9 +86,6 @@
 	 */
 	private function parseOperationType(string $type): array
 	{
-		// Remove BASE prefix if present (e.g. 'jvb_helcim_sync_to' → 'helcim_sync_to')
-		$type = str_replace(BASE, '', $type);
-
 		$pos = strpos($type, '_');
 		if ($pos === false) {
 			throw new Exception("Invalid integration operation type: {$type}");
@@ -103,21 +100,12 @@
 	/**
 	 * Resolve integration instance, optionally for a specific user
 	 */
-	private function resolveIntegration(string $serviceName, int $userId): ?object
+	private function resolveIntegration(string $serviceName, ?int $userId = null): ?object
 	{
-		if (!isset($this->integrationCache[$serviceName])) {
-			$this->integrationCache[$serviceName] = JVB()->connect($serviceName);
+		if ($userId === 0) {
+			$userId = null;
 		}
-
-		$integration = $this->integrationCache[$serviceName];
-
-		// If operation has a user context, re-instantiate for that user
-		if ($integration && $userId) {
-			$class = get_class($integration);
-			return new $class($userId);
-		}
-
-		return $integration;
+		return JVB()->connect($serviceName, $userId);
 	}
 
 	/*****************************************************************
@@ -137,6 +125,11 @@
 			return new Result(outcome: 'success', result: ['synced' => [], 'message' => 'No items to sync']);
 		}
 
+		if ($integration->hasBatchUpdate()) {
+			$result = $integration->updateBatchToService($data['items']);
+			return new Result(outcome: $result['outcome'], result: $result['result']);
+		}
+
 		foreach ($items as $postID) {
 			try {
 				$result = $integration->syncPostToService((int)$postID);
diff --git a/inc/registrar/Registrar.php b/inc/registrar/Registrar.php
index 58590ba..b30b0cf 100644
--- a/inc/registrar/Registrar.php
+++ b/inc/registrar/Registrar.php
@@ -680,8 +680,8 @@
 	public static function withIntegration(string $integration, ?string $type = null):array
 	{
 		self::ensureInstanced();
-
-		if (!Site::has($integration)) {
+		Site::getInstance();
+		if (!Site::hasIntegration($integration)) {
 			error_log('[Registrar]::withIntegration Integration not available to fetch: '.$integration);
 			return [];
 		}
@@ -690,7 +690,7 @@
 			if (!is_null($type) && $inst->type !== $type) {
 				return false;
 			}
-			return array_key_exists($integration, $this->integrationConfigs);
+			return array_key_exists($integration, $inst->integrationConfigs);
 		}));
 	}
 
diff --git a/inc/registrar/config/Integration.php b/inc/registrar/config/Integration.php
index cab3a4c..6316e6c 100644
--- a/inc/registrar/config/Integration.php
+++ b/inc/registrar/config/Integration.php
@@ -27,6 +27,7 @@
 	 * @var bool whether this is a customer role, used if this is a user-based Registrar
 	 */
 	protected bool $isCustomer;
+	protected ?string $category = null;
 
 
 
@@ -73,6 +74,14 @@
 		return $this->content_type;
 	}
 
+	public function setCategory(string $category):void
+	{
+		$check = Registrar::getInstance($category);
+		if ($check) {
+			$this->category = $check->getBased();
+		}
+	}
+
 	public function setInitial(bool $set):self
 	{
 		$this->initial = $set;
diff --git a/inc/registrar/helpers/AddIntegrationFields.php b/inc/registrar/helpers/AddIntegrationFields.php
index bf615e7..6aad5fa 100644
--- a/inc/registrar/helpers/AddIntegrationFields.php
+++ b/inc/registrar/helpers/AddIntegrationFields.php
@@ -73,6 +73,7 @@
 				'type'	=> 'true_false',
 				'label'	=> 'Share To '.$this->allowed[$this->service_name],
 				'section'	=> 'sync',
+				'default'	=> 1
 			]
 		];
 		if ($this->config->getUpdate()){
@@ -84,7 +85,8 @@
 					'field'	=> 'share_to_'.$this->service_name,
 					'value'	=> 1,
 					'operator'	=> '=='
-				]
+				],
+				'default'	=> 1,
 			];
 
 			$fields["_{$this->service_name}_item_id"] = [
@@ -128,7 +130,8 @@
 					'field' => 'share_to_'.$this->service_name,
 					'operator' => '==',
 					'value' => 1
-				]
+				],
+				'default'	=> 1,
 			];
 		}
 		$additional = JVB()->connect($this->service_name)->getAdditionalFields($this->config->getContentType());

--
Gitblit v1.10.0