From de317675a8069b747cb253ba3e2b5dc394ca36ef Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sat, 18 Jul 2026 21:44:51 +0000
Subject: [PATCH] =Base Integrations mainly done - doing a test with setting up the Square integration to follow

---
 inc/integrations/services/HelcimOld.php           | 1359 ++++
 inc/integrations/services/UmamiOld.php            |  803 ++
 inc/integrations/Instance.php                     |   17 
 inc/integrations/services/GoogleMyBusiness.php    |    1 
 inc/integrations/services/FacebookOld.php         |  701 ++
 inc/integrations/_Base.php                        |   59 
 inc/integrations/services/InstagramOld.php        |  705 ++
 inc/integrations/services/PostMarkOld.php         |  661 ++
 inc/integrations/SyncTo.php                       |  455 +
 inc/integrations/services/CloudflareOld.php       |  266 
 inc/integrations/_setup.php                       |   12 
 inc/integrations/services/BlueSkyOld.php          |  411 +
 inc/integrations/services/Helcim.php              |    2 
 inc/integrations/Admin.php                        |   13 
 inc/integrations/OAuth.php                        |   94 
 inc/integrations/SyncDefaults.php                 |  171 
 inc/registrar/helpers/AddIntegrationFields.php    |    2 
 inc/integrations/Field.php                        |   38 
 inc/integrations/services/Square.php              | 3772 +----------
 inc/integrations/Integrations.php                 | 1656 -----
 inc/integrations/services/SquareOld.php           | 3506 +++++++++++
 inc/integrations/services/GoogleMyBusinessOld.php | 2974 +++++++++
 inc/integrations/OAuthURLs.php                    |   26 
 inc/integrations/SyncHelpers.php                  |   47 
 inc/integrations/OAuthCredentials.php             |    3 
 inc/integrations/UserConnection.php               |    8 
 inc/integrations/services/GoogleMapsOld.php       |  834 ++
 27 files changed, 13,606 insertions(+), 4,990 deletions(-)

diff --git a/inc/integrations/Admin.php b/inc/integrations/Admin.php
index 122a459..3f321a5 100644
--- a/inc/integrations/Admin.php
+++ b/inc/integrations/Admin.php
@@ -78,15 +78,8 @@
 		return $result;
 	}
 
-	public function response(bool $success, string $message):array
-	{
-		return [
-			'success'	=> $success,
-			'message'	=> $message
-		];
-	}
 
-	public function renderConnection():string
+	public function renderConnection(bool $return = false):string
 	{
 		if ($this->userID && !JVB()->userCanConnect($this->service_name, $this->userID)) {
 			return '';
@@ -176,6 +169,10 @@
 		$form .= $this->outputActionButtons();
 		$form .= '</form>';
 
+		if (!$return) {
+			echo $form;
+		}
+
 		return $form;
 	}
 		protected function outputActionButtons():string
diff --git a/inc/integrations/Field.php b/inc/integrations/Field.php
index 9217387..788400b 100644
--- a/inc/integrations/Field.php
+++ b/inc/integrations/Field.php
@@ -11,9 +11,13 @@
 	public string $name;
 	public string $label;
 	public string $type;
+	public string $hint;
 	public bool $required;
 	public string|Closure|bool $permission;
 	protected array $options;
+	protected mixed $default;
+	protected string $subtype;
+	protected bool $hidden = false;
 
 	public function __construct(string $name, string $label, string $type, callable|string|bool $permission)
 	{
@@ -49,6 +53,24 @@
 	{
 		$this->options = $options;
 	}
+	public function setDefault(mixed $default):void
+	{
+		$this->default = $default;
+	}
+
+	public function setHint(string $hint):void
+	{
+		$this->hint = $hint;
+	}
+	public function setHidden(bool $hidden = true):void
+	{
+		$this->hidden = $hidden;
+	}
+
+	public function setSubType(string $type):void
+	{
+		$this->subType = $type;
+	}
 
 	public function getConfig():array
 	{
@@ -57,12 +79,24 @@
 			'label'	=> $this->label,
 			'type'	=> $this->type,
 		];
-		if ($this->required) {
+		if (isset($this->required)) {
 			$conf['required'] = true;
 		}
-		if ($this->options) {
+		if (isset($this->options)) {
 			$conf['options'] = $this->options;
 		}
+		if (isset($this->default)) {
+			$conf['default'] = $this->default;
+		}
+		if (isset($this->hint)) {
+			$conf['hint'] = $this->hint;
+		}
+		if (isset($this->subType)) {
+			$conf['subtype'] = $this->subType;
+		}
+		if ($this->hidden) {
+			$conf['hidden'] = true;
+		}
 		return $conf;
 	}
 }
diff --git a/inc/integrations/Instance.php b/inc/integrations/Instance.php
new file mode 100644
index 0000000..28d5e66
--- /dev/null
+++ b/inc/integrations/Instance.php
@@ -0,0 +1,17 @@
+<?php
+namespace JVBase\integrations;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+trait Instance {
+	protected static self $instance;
+	public static function getInstance():self
+	{
+		if (!isset(self::$instance)) {
+			self::$instance = new self();
+		}
+		return self::$instance;
+	}
+}
diff --git a/inc/integrations/Integrations.php b/inc/integrations/Integrations.php
index dd8ede2..f9a34ef 100644
--- a/inc/integrations/Integrations.php
+++ b/inc/integrations/Integrations.php
@@ -3,17 +3,9 @@
 
 use Exception;
 use JVBase\managers\Cache;
-use JVBase\managers\ErrorHandler;
-use JVBase\managers\queue\executors\IntegrationExecutor;
-use JVBase\managers\queue\mergers\DefaultMerger;
-use JVBase\managers\queue\TypeConfig;
 use JVBase\managers\UploadManager;
-use JVBase\meta\Form;
-use JVBase\meta\Meta;
-use JVBase\registrar\helpers\AddIntegrationFields;
 use JVBase\registrar\Registrar;
 use WP_Error;
-use WP_Post;
 
 if (!defined('ABSPATH')) {
 	exit;
@@ -32,13 +24,6 @@
 {
 	use _Base;
 
-	//Flag to allow for custom settings (defaults, etc) for an integration in the dashboard
-	public bool $hasExtraOptions = false;
-	public bool $hasBatchCreate = false;
-	public bool $hasBatchUpdate = false;
-	public bool $hasBatchDelete = false;
-	public bool $canCreateOnUpdate = false;
-
 	/**
 	 * API Configuration
 	 * These properties define how the integration connects to external services
@@ -52,16 +37,11 @@
 	/**
 	 * Credentials & State
 	 */
-
-	protected string $defaultContent = 'post'; //Default integration content type, is none is set. MUST EXIST as array key in $this->>getContentTypes
-
 	protected array $allowedContent = [];
 
 	/**
 	 * Caching Configuration
 	 */
-	protected ?string $cacheName = null;
-	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)
@@ -69,26 +49,6 @@
 		'none' => 0           // No caching for real-time data
 	];
 
-	/**
-	 * Post Syncing Capabilities
-	 * Define what sync operations this integration supports
-	 */
-	protected array $canSync = [
-		'initial' => false,    // Can share new posts to the service
-		'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 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
-	/**
-	 * Error Handling Configuration
-	 */
-	protected array $lastError = [];
-	protected array $retryDelays = [1, 2, 5]; // Exponential backoff in seconds
-
-
 
 	protected function __construct(?int $userID = null)
 	{
@@ -98,7 +58,11 @@
 
 		$this->getPostTypes();
 		$this->getTaxonomies();
-		$this->setContentTypes();
+		$this->getUsers();
+
+		if (method_exists($this, 'setContentTypes')) {
+			$this->setContentTypes();
+		}
 		$this->registerHooks();
 
 		if (method_exists($this, 'setQueueTypes')) {
@@ -106,193 +70,30 @@
 		}
 
 		$this->initializeRateLimiters();
+
 		if (method_exists($this, 'initializeActions')) {
 			$this->initializeActions();
 		}
 		if (method_exists($this, 'addOAuthActions')) {
 			$this->addOAuthActions();
 		}
-
-		add_filter('jvbShouldRenderMeta', [$this, 'checkRenderField'], 10, 4);
-	}
-		protected function initializeRateLimiters():void
-		{
-			$key = $this->service_name;
-			if (!is_null($this->userID)) {
-				$key .= '_'.$this->userID;
-			}
-
-			if ($this->hasRequests && !isset($this->requestLimiter) && !is_null($this->oauthLimiter)) {
-				$this->requestLimiter = new RateLimits($key);
-			}
-			if ($this->hasOAuth && !isset($this->oauthLimiter) && !is_null($this->oauthLimiter)) {
-				$key .= '_oauth';
-				$this->oauthLimiter = new RateLimits($key, ['s' => 1,'m' => 10, 'h' => 100]);
-			}
-		}
-
-
-	protected function setContentTypes():void
-	{
-
-	}
-	public function getContentTypes(string $type):array
-	{
-		return (array_key_exists($type, $this->contentTypes)) ? $this->contentTypes[$type] : $this->contentTypes;
-	}
-
-	public function checkRenderField($shouldRender, $name, $type, $objectType):bool
-	{
-		if ($type !== 'form') {
-			return $shouldRender;
-		}
-
-		if (!$this->isSetUp() && str_contains($name, $this->service_name)) {
-			return false;
-		}
-		return $shouldRender;
-	}
-
-	public function handleOAuthConnect(): array
-	{
-		if (!$this->hasOAuth) {
-			return ['success' => false, 'message' => 'Not an OAuth service'];
-		}
-
-		// This would typically redirect to OAuth provider
-		// Child classes can override for specific behavior
-		$auth_url = $this->getOAuthUrl();
-
-		if ($auth_url) {
-			return [
-				'success' => true,
-				'redirect' => $auth_url,
-			];
-		}
-
-		return ['success' => false, 'message' => 'Failed to generate OAuth URL'];
-	}
-
-	/**
-	 * Check if OAuth token is valid and not expired
-	 * @return bool
-	 */
-	public function isOAuthValid(): bool
-	{
-		if (!$this->isOAuthService) {
-			return false;
-		}
-
-		// Check if we have tokens
-		if (empty($this->credentials['access_token'])) {
-			return false;
-		}
-
-		// Check token expiry if stored
-		if (!empty($this->credentials['expires_at'])) {
-			$expires_at = intval($this->credentials['expires_at']);
-			if ($expires_at <= time()) {
-				return false;
-			}
-		}
-
-		// For services without expiry info, do a test API call
-		return true;
-//		return $this->testConnection();
-	}
-
-
-
-	public function getOAuthUrlAction(array $data = []): array
-	{
-		if (!$this->isOAuthService) {
-			return ['success' => false, 'message' => 'Not an OAuth service'];
-		}
-
-		$return_url = $data['return_url'] ?? null;
-		$auth_url = $this->getOAuthUrl($return_url);
-
-		if ($auth_url) {
-			return [
-				'success' => true,
-				'auth_url' => $auth_url,
-				'popup' => true
-			];
-		}
-
-		return ['success' => false, 'message' => 'Failed to generate OAuth URL'];
 	}
 
 	protected function getPostTypes(): void
 	{
-		$this->syncPostTypes = Registrar::withIntegration($this->service_name);
+		$this->syncPostTypes = Registrar::withIntegration($this->service_name, 'post');
+	}
+
+	protected function getUsers():void
+	{
+		$this->syncUsers = Registrar::withIntegration($this->service_name, 'user');
 	}
 
 	protected function getTaxonomies():void
 	{
-		$key = BASE . $this->service_name . '_sync_taxonomies';
-		$taxonomies = get_option($key, false);
-
-		if (!$taxonomies) {
-			// Combine both content and taxonomy filtering
-			$taxonomies = [];
-			foreach (Registrar::withFeature('is_content', 'term') as $type) {
-				$registrar = Registrar::getInstance($type);
-				if ($registrar->hasIntegration($this->service_name)) {
-					$taxonomies[] = $registrar->getSlug();
-				}
-			}
-
-			update_option($key, $taxonomies);
-		}
-
-		$this->syncTaxonomies = $taxonomies;
+		$this->syncTaxonomies = Registrar::withIntegration($this->service_name, 'term');
 	}
 
-	protected function getRedirectUri(): string
-	{
-//		if (!empty($this->oauth['redirect_uri'])) {
-//			return $this->oauth['redirect_uri'];
-//		}
-		if ($this->hasOAuth) {
-			return admin_url('admin-ajax.php?action=' . BASE . $this->service_name . '_oauth_callback');
-		}
-		// Changed from admin-ajax.php to REST endpoint
-		return rest_url('jvb/v1/oauth/callback?service=' . $this->service_name);
-	}
-
-	/**
-	 * Used by IntegrationsRoutes.php
-	 * @param string $code
-	 * @param string $state
-	 * @return array
-	 */
-	public function handleOAuthCode(string $code, string $state): array
-	{
-		try {
-			$this->loadCredentials();
-			$tokens = $this->exchangeOAuthCode($code);
-
-			if (!$tokens) {
-				return ['success' => false, 'message' => 'Failed to exchange authorization code'];
-			}
-
-			$credentials = array_merge($this->credentials, $tokens);
-			$credentials = $this->addCredentialData($credentials, $tokens);
-			$saved = $this->saveCredentials($credentials, false);
-
-			if ($saved) {
-				return ['success' => true, 'message' => 'Successfully connected'];
-			}
-
-			return ['success' => false, 'message' => 'Failed to save credentials'];
-		} catch (Exception $e) {
-			$this->logError('OAuth code exchange failed', ['error' => $e->getMessage()]);
-			return ['success' => false, 'message' => 'OAuth error: ' . $e->getMessage()];
-		}
-	}
-
-
 	/*********************************************************************
 	 * ABSTRACT METHODS - MUST BE IMPLEMENTED BY CHILD CLASSES
 	 *********************************************************************/
@@ -338,33 +139,20 @@
 		try {
 			// Process and validate credentials
 			if (!$this->validateCredentials($credentials)) {
-				$this->logError('Invalid credentials');
-				return [
-					'success'	=> false,
-					'message'	=> 'Credentials not formatted correctly'
-				];
+				$this->logError('saveCredentials', 'Invalid credentials');
+				return $this->response(false, 'Credentials not formatted correctly');
 			}
 
+			//Merge the new credentials with the old ones (new stuff overriding any old stuff)
 			$old = $this->getCredentials();
 			$credentials = array_merge($old, $credentials);
 
-			// Temporarily set credentials for testing
-			$this->credentials = $credentials;
-			$this->initialize();
-
 			// Test the connection before saving
 			if ($test) {
 				if (!$this->testConnection(true)) {
-					$this->logError('Connection test failed');
-					// Revert to old credentials
-					$this->credentials = $old;
-					$this->initialize();
+					$this->logError('saveCredentials', 'Connection test failed');
 
-					return [
-						'success' => false,
-						'message' => 'Connection failed. Please check your credentials.',
-						'test_failed' => true
-					];
+					return $this->response(false, 'Connection failed. Please check your credentials');
 				}
 			}
 
@@ -380,17 +168,10 @@
 				$this->updateLastTestedTime();
 				$this->clearCache();
 
-				return [
-					'success' => true,
-					'message' => 'Credentials validated and saved successfully',
-					'reload' => true
-				];
+				return $this->response(true, 'Credentials validated and saved successfully');
 			}
 
-			return [
-				'success' => false,
-				'message' => 'Failed to save credentials to database'
-			];
+			return $this->response(false, 'Failed to save credentials to database');
 
 		} catch (\Exception $e) {
 			// Revert to old credentials on any error
@@ -398,13 +179,7 @@
 				$this->service_name,
 				$this->userID
 			);
-			$this->credentials = $old;
-			$this->initialize();
-
-			return [
-				'success' => false,
-				'message' => 'Validation error: ' . $e->getMessage()
-			];
+			return $this->response(false, 'Validation error: '.print_r($e->getMessage(), true));
 		}
 	}
 
@@ -414,9 +189,7 @@
 	public function deleteCredentials():array
 	{
 		$success = Auth::getInstance()->deleteCredentials($this->service_name, $this->userID);
-		return [
-			'success'	=> $success
-		];
+		return $this->response(true, 'Deleted successfully');
 	}
 	/**
 	 * Validate credentials before storing
@@ -543,52 +316,6 @@
 	/******************************************************************
 		POST SYNC
 	 ******************************************************************/
-	/**
-	 * Handle post save for syncing
-	 *
-	 * Override to implement custom sync logic when posts are saved.
-	 * Check the $settings array for post type specific configuration.
-	 *
-	 * @param int $postID The post ID
-	 * @param WP_Post $post The post object
-	 * @param bool $update Whether this is an update
-	 * @param array $settings Post type integration settings
-	 * @return void
-	 */
-	protected function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings): void
-	{
-		// Override in child classes that support post syncing
-		// Example implementation:
-		/*
-		$fields = $this->getSyncFields($postID, 'post', ['schedule_' . $this->service_name]);
-		$options = $fields['schedule_' . $this->service_name] !== ''
-			? ['scheduled' => strtotime($fields['schedule_' . $this->service_name])]
-			: [];
-
-		$this->queueOperation(
-			'sync_post',
-			['post_id' => $postID, 'is_update' => $update],
-			$options
-		);
-		*/
-	}
-
-	/*********************************************************************
-	 * API REQUEST METHODS
-	 *********************************************************************/
-
-
-
-
-
-
-
-
-
-
-
-
-
 
 	/*********************************************************************
 	 * SYNC METHODS
@@ -668,7 +395,8 @@
 		// Let child classes register additional hooks if needed
 		$this->registerAdditionalHooks();
 
-		if (!empty($this->syncPostTypes) && is_null($this->userID)) {
+
+		if (!empty($this->syncPostTypes)) {
 			$this->addSavePost();
 			add_action('transition_post_status', [$this, 'handlePostStatusTransition'], 10, 3);
 
@@ -676,64 +404,21 @@
 				add_action('before_delete_post', [$this, 'handleDeletePost'], 10, 1);
 			}
 		}
-		if (!empty($this->syncTaxonomies) && is_null($this->userID)) {
+		if (!empty($this->syncTaxonomies)) {
 			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);
+		if (method_exists($this, 'registerQueueTypes')) {
+			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.
-			}
 
 	/**
 	 * Handle connection setup and credential storage
@@ -755,11 +440,11 @@
 			}
 
 			// Store credentials
-			$this->credentials = array_merge($this->credentials ?? [], $sanitized);
-			$this->credentials['last_updated'] = time();
+			$credentials = array_merge($this->loadCredentials() ?? [], $sanitized);
+			$credentials['last_updated'] = time();
 
 			// Save to database
-			$saved = $this->saveCredentials($this->credentials);
+			$saved = $this->saveCredentials($credentials);
 
 			if (!$saved) {
 				return [
@@ -810,6 +495,7 @@
 
 	/**
 	 * Ensure service is initialized
+	 * TODO: I don't think this is necessary anymore.
 	 */
 	protected function ensureInitialized(): void
 	{
@@ -819,404 +505,6 @@
 		$this->initialize();
 	}
 
-
-	/***************************************************************
-		ERROR HANDLING
-	 ***************************************************************/
-	/**
-	 * Handle error responses
-	 */
-	protected function handleApiError(int $code, string $body, string $endpoint): void
-	{
-		$message = "API Error ({$code}): ";
-		$decoded = json_decode($body, true);
-
-		// Extract error details
-		$error_details = $this->extractErrorDetails($decoded, $body);
-		$message .= $error_details['message'];
-
-		// Determine error severity based on HTTP code
-		$severity = $this->getErrorSeverity($code);
-
-		// Build comprehensive error context
-		$error_context = [
-			'service' => $this->service_name,
-			'endpoint' => $endpoint,
-			'http_code' => $code,
-			'error_type' => $this->categorizeApiError($code),
-			'error_details' => $error_details,
-			'user_id' => $this->userID,
-			'request_time' => time(),
-			'consecutive_errors' => $this->error_stats['consecutive_errors'],
-			'is_oauth' => $this->isOAuthService,
-			'api_version' => $this->apiVersion,
-			'integration_healthy' => $this->is_healthy
-		];
-
-		// Add rate limit information if present
-		if ($code === 429) {
-			$error_context['rate_limit'] = $this->extractRateLimitInfo($decoded, $body);
-		}
-
-		// Log to ErrorHandler with proper severity
-		$this->logError($message, $error_context, $severity);
-
-		// Update error statistics
-		$this->updateErrorStats($code, $endpoint);
-
-		// Store last error for debugging
-		$this->lastError = [
-			'code' => $code,
-			'message' => $message,
-			'endpoint' => $endpoint,
-			'timestamp' => time(),
-			'context' => $error_context
-		];
-	}
-
-
-	/*****************************************************************
-		OAUTH
-	 *****************************************************************/
-
-	/**
-	 * Get OAuth authorization URL
-	 */
-	public function getOAuthUrl(?string $return_url = null): string
-	{
-
-		if (!$this->hasOAuth) {
-			return '';
-		}
-
-		if (empty($this->credentials)) {
-			$this->ensureInitialized();
-		}
-
-		if (empty($this->oauth['authorize'])) {
-			$this->logError('OAuth authorize URL not configured');
-			return '';
-		}
-
-		// Build base parameters
-		$params = [
-			'client_id' => $this->credentials['client_id'] ?? $this->credentials['app_id'] ?? '',
-			'redirect_uri' => $this->getRedirectUri(),
-			'response_type' => 'code',
-			'scope' => implode(' ', $this->oauth['scopes'] ?? [])
-		];
-		$state_key = wp_generate_password(32, false);
-		$user_id = $this->userID??0;
-
-		// Store state data in transient (expires in 10 minutes)
-		set_transient(
-			'oauth_state_' . $state_key,
-			[
-				'service' => $this->service_name,
-				'user_id' => $user_id,
-				'created' => time()
-			],
-			600
-		);
-
-		$state_parts = [
-			$state_key,
-			$user_id
-		];
-
-		// Add return URL if provided
-		if ($return_url) {
-			$state_parts[] = base64_encode($return_url);
-		} else {
-			$state_parts[] = base64_encode(admin_url('admin.php?page=jvb-integrations'));
-		}
-
-		$params['state'] = implode('|', $state_parts);
-
-		// Allow child classes to modify params (they can override/remove as needed)
-		if (method_exists($this, 'addOAuthParams')) {
-			$params = $this->addOAuthParams($params);
-		}
-
-		return $this->oauth['authorize'] . '?' . http_build_query($params);
-	}
-
-	/**
-	 * Add service-specific OAuth parameters
-	 */
-	protected function addOAuthParams(array $params): array
-	{
-		// Override in child classes to add service-specific params
-		// e.g., access_type, prompt, etc.
-		return $params;
-	}
-
-
-
-	/**
-	 * Extract retry-after header from response
-	 *
-	 * @param array|WP_Error $response WordPress HTTP response
-	 * @return int Seconds to wait before retry
-	 */
-	protected function extractRetryAfter($response): int
-	{
-		if (is_wp_error($response)) {
-			return 5;
-		}
-
-		$headers = wp_remote_retrieve_headers($response);
-
-		if (isset($headers['retry-after'])) {
-			// Could be seconds or HTTP date
-			$retry_after = $headers['retry-after'];
-
-			if (is_numeric($retry_after)) {
-				return (int) $retry_after;
-			}
-
-			// Try to parse as date
-			$timestamp = strtotime($retry_after);
-			if ($timestamp !== false) {
-				return max(0, $timestamp - time());
-			}
-		}
-
-		return 5; // Default wait time
-	}
-
-	/**
-	 * Exchange OAuth code for tokens
-	 */
-	protected function exchangeOAuthCode(string $code): ?array
-	{
-		$this->ensureInitialized();
-
-		// Build request data
-		$request_data = [
-			'client_id' => $this->credentials['client_id'] ?? '',
-			'client_secret' => $this->credentials['client_secret'] ?? '',
-			'code' => $code,
-			'grant_type' => 'authorization_code',
-			'redirect_uri' => $this->getRedirectUri()
-		];
-
-		$oauth_endpoint = $this->oauth['token'];
-		$response = $this->makeOAuthRequest('POST', $oauth_endpoint, $request_data);
-
-		if (is_wp_error($response)) {
-			$this->logError('OAuth token exchange failed', [
-				'error' => $response->get_error_message(),
-				'code' => $response->get_error_code()
-			]);
-			return null;
-		}
-
-		// Parse response
-		if (isset($response['access_token'])) {
-			$expires_in = $response['expires_in'] ?? 2592000; // 30 days default
-
-			return [
-				'access_token' => $response['access_token'],
-				'refresh_token' => $response['refresh_token'] ?? '',
-				'expires_in' => $expires_in,
-				'expires_at' => time() + $expires_in, // Calculate expiry timestamp
-				'token_type' => $response['token_type'] ?? 'Bearer',
-				'merchant_id' => $response['merchant_id'] ?? '',
-				'scope' => $response['scope'] ?? ''
-			];
-		}
-
-		$this->logError('Failed to obtain access token', ['response' => $response]);
-		return null;
-	}
-
-	/**
-	 * Add service-specific credential data
-	 */
-	protected function addCredentialData(array $credentials, array $tokens): array
-	{
-		// Override in child classes to add service-specific data
-		return $credentials;
-	}
-
-	/**
-	 * Check if token should be proactively refreshed
-	 * Different from isOAuthValid() which checks if token is actually expired
-	 */
-	protected function shouldRefreshToken(): bool
-	{
-		if (!$this->hasOAuth || $this->refresh_interval === 0) {
-			return false;
-		}
-
-		// If no expiry info, we can't proactively refresh
-		if (empty($this->credentials['expires_at'])) {
-			return false;
-		}
-
-		$expires_at = intval($this->credentials['expires_at']);
-		$time_until_expiry = $expires_at - time();
-
-		// Refresh if we're within the refresh interval window
-		return $time_until_expiry > 0 && $time_until_expiry <= $this->refresh_interval;
-	}
-	/**
-	 * Get time until token refresh is recommended
-	 * Useful for displaying in admin UI
-	 */
-	public function getTimeUntilRefresh(): ?int
-	{
-		if ($this->refresh_interval === 0 || empty($this->credentials['expires_at'])) {
-			return null;
-		}
-
-		$expires_at = intval($this->credentials['expires_at']);
-		$refresh_at = $expires_at - $this->refresh_interval;
-		$time_until_refresh = $refresh_at - time();
-
-		return max(0, $time_until_refresh);
-	}
-
-	/**
-	 * Get token freshness status
-	 * Returns: 'fresh', 'should_refresh', 'expired', or 'no_expiry_info'
-	 */
-	public function getTokenStatus(): string
-	{
-		if (!$this->hasOAuth) {
-			return 'not_oauth';
-		}
-
-		if (empty($this->credentials['access_token'])) {
-			return 'no_token';
-		}
-
-		if (empty($this->credentials['expires_at'])) {
-			return 'no_expiry_info';
-		}
-
-		$expires_at = intval($this->credentials['expires_at']);
-		$now = time();
-
-		if ($expires_at <= $now) {
-			return 'expired';
-		}
-
-		if ($this->shouldRefreshToken()) {
-			return 'should_refresh';
-		}
-
-		return 'fresh';
-	}
-	/**
-	 * Refresh OAuth token
-	 */
-	protected function refreshOAuthToken(): bool
-	{
-		if (!$this->hasOAuth || empty($this->credentials['refresh_token'])) {
-			return false;
-		}
-
-		$request_data = [
-			'client_id' => $this->credentials['client_id'],
-			'client_secret' => $this->credentials['client_secret'],
-			'refresh_token' => $this->credentials['refresh_token'],
-			'grant_type' => 'refresh_token'
-		];
-
-		$response = $this->makeOAuthRequest('POST', $this->oauth['token'], $request_data);
-
-		if (is_wp_error($response)) {
-			$error_message = $response->get_error_message();
-
-			if (str_contains($error_message, 'invalid_grant')) {
-				$this->logError('OAuth refresh token is invalid - user must re-authorize', [
-					'error' => $error_message
-				], 'critical');
-
-				// Mark unhealthy immediately
-				$this->error_stats['consecutive_errors'] = $this->error_threshold;
-				$this->is_healthy = false;
-				$this->saveErrorStats();
-			}
-
-			$this->logError('Failed to refresh OAuth token for '.$this->service_name, [
-				'error' => $error_message
-			]);
-			return false;
-		}
-
-		if (isset($response['access_token'])) {
-			$this->credentials['access_token'] = $response['access_token'];
-			$this->credentials['expires_at'] = time() + ($response['expires_in'] ?? 2592000); // 30 days
-
-			// Note: Some services return the SAME refresh token
-			if (isset($response['refresh_token'])) {
-				$this->credentials['refresh_token'] = $response['refresh_token'];
-			}
-
-			$this->saveCredentials($this->credentials);
-			return true;
-		}
-
-		return false;
-	}
-
-	/**
-	 * Revoke OAuth access
-	 */
-	public function revokeOAuthAccess(): bool
-	{
-		if (!$this->hasOAuth || empty($this->oauth['revoke'])) {
-			return false;
-		}
-
-		if (!empty($this->credentials['access_token'])) {
-			wp_remote_post($this->oauth['revoke'], [
-				'body' => ['token' => $this->credentials['access_token']]
-			]);
-		}
-
-		return Auth::getInstance()->deleteCredentials($this->service_name, $this->userID);
-
-	}
-
-	public function handleOAuthDisconnect(): array
-	{
-		try {
-			// Revoke the token with Square using centralized request
-			if (!empty($this->credentials['access_token'])) {
-				$revoke_data = [
-					'client_id' => $this->credentials['client_id'] ?? '',
-					'access_token' => $this->credentials['access_token']
-				];
-
-				// Make revoke request (ignore response as revoke often returns empty)
-				$this->makeOAuthRequest('POST', $this->oauth['revoke'], $revoke_data);
-			}
-
-			// Clear stored credentials (preserve app credentials)
-			$this->credentials = [
-				'client_id' => $this->credentials['client_id'] ?? '',
-				'client_secret' => $this->credentials['client_secret'] ?? '',
-				'environment' => $this->credentials['environment'] ?? 'sandbox'
-			];
-
-			$this->saveCredentials($this->credentials);
-			$this->clearCache();
-
-			return [
-				'success' => true,
-				'message' => 'Successfully disconnected from Square'
-			];
-		} catch (Exception $e) {
-			return [
-				'success' => false,
-				'message' => 'Failed to disconnect: ' . $e->getMessage()
-			];
-		}
-	}
 	/**
 	 * Generate webhook signature key for services that require it
 	 * @return string
@@ -1225,268 +513,6 @@
 	{
 		return wp_generate_password(32, false);
 	}
-	/**
-	 * Generate OAuth state parameter
-	 */
-	protected function generateOAuthState(?int $user_id, ?string $return_url = null): string
-	{
-		$user_id = $user_id ?? 0;
-		$state = wp_create_nonce($this->service_name . '_oauth_' . $user_id) . '|' . $user_id;
-
-		if ($return_url) {
-			$state .= '|' . base64_encode($return_url);
-		}
-
-		return $state;
-	}
-
-	protected function getNonce(): string
-	{
-		 return wp_create_nonce($this->service_name . '_oauth_' . $this->userID);
-	}
-
-	/**
-	 * Determine return URL after OAuth
-	 */
-	protected function determineReturnUrl(?int $user_id): string
-	{
-		if ($user_id > 0) {
-			return home_url('/dash/integrations/#' . $this->service_name);
-		}
-
-		return admin_url('admin.php?page=jvb-integrations');
-	}
-
-
-	/****************************************************************
-		POST SYNC
-	 ****************************************************************/
-	/**
-	 * Get field mapping for a post type
-	 */
-	protected function getFieldMapping(string $post_type): array
-	{
-		// Apply filter for custom mapping
-		return apply_filters(
-			"jvb_{$this->service_name}_field_mapping",
-			[],
-			$post_type,
-			$this
-		);
-	}
-
-
-	/**
-	 * Map WordPress fields to service fields
-	 */
-	protected function mapFieldsToService(int $postID, array $mapping): array
-	{
-		$meta_manager = Meta::forPost($postID);
-		$service_data = [];
-
-		foreach ($mapping as $wp_field => $service_field) {
-			$value = $meta_manager->get($wp_field);
-
-			if ($value !== null && $value !== '') {
-				$this->setNestedValue($service_data, $service_field, $value);
-			}
-		}
-
-		return apply_filters(
-			"jvb_{$this->service_name}_mapped_data",
-			$service_data,
-			$postID,
-			$mapping
-		);
-	}
-
-	/**
-	 * Set nested array value using dot notation
-	 */
-	protected function setNestedValue(array &$array, string $path, $value): void
-	{
-		$keys = explode('.', $path);
-		$current = &$array;
-
-		foreach ($keys as $i => $key) {
-			if ($i === count($keys) - 1) {
-				$current[$key] = $value;
-			} else {
-				if (!isset($current[$key])) {
-					$current[$key] = [];
-				}
-				$current = &$current[$key];
-			}
-		}
-	}
-	/**
-	 * Handle post save
-	 */
-	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;
-		}
-		$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;
-		}
-
-		$registrar = Registrar::getInstance($postType);
-		if (!$registrar){
-			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();
-	}
-
-
-	protected function getSyncFields(int $postID, string $type, array $additional = []):array
-	{
-		$meta = new Meta($postID, $type);
-		$fieldsToCheck = [
-			'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",
-			... $additional
-		];
-		return $meta->getAll($fieldsToCheck);
-	}
-
-	/**
-	 * Handle post status transitions
-	 */
-	public function handlePostStatusTransition(string $new_status, string $old_status, WP_Post $post): void
-	{
-		if (empty($this->syncPostTypes)) {
-			return;
-		}
-
-		if (!in_array(jvbNoBase($post->post_type), $this->syncPostTypes)) {
-			return;
-		}
-
-		//Map fields from our custom post types to the fields expected by the integration
-		$mappedFields = $this->getFieldMapping($post->post_type);
-
-		$fields = $this->getSyncFields($post->ID, 'post', $mappedFields);
-
-		// Handle unpublish action
-		if ($old_status === 'publish' && $new_status !== 'publish') {
-
-			if ($fields["_{$this->service_name}_item_id"] && $this->canSync['delete']) {
-				try {
-					$this->queueOperation(
-						'delete_post',
-						[
-							$post,
-							$fields["_{$this->service_name}_item_id"]
-						]
-					);
-				} catch (Exception $e) {
-					$this->logError("Failed to handle unpublish for post {$post->ID}: " . $e->getMessage());
-				}
-			}
-		}
-	}
-
-	/**
-	 * Handle post deletion
-	 */
-	public function handleDeletePost(int $postID): void
-	{
-		if (!$this->canSync['delete']) {
-			return;
-		}
-
-		$post = get_post($postID);
-		if (!$post || !in_array($post->post_type, $this->syncPostTypes)) {
-			return;
-		}
-
-		$fields = $this->getSyncFields($postID, 'post');
-
-		if ($fields["_{$this->service_name}_item_id"] !== '') {
-
-			$this->queueOperation(
-				'delete_post',
-				[
-					'post_id'	=> $post->ID,
-				]
-			);
-		}
-	}
-
-	protected function handleSaveTerm($term_id, $tt_id, $taxonomy, $update, $args): void
-	{
-		$noBase = jvbNoBase($taxonomy);
-		if (!in_array($noBase, $this->syncTaxonomies)) {
-			return;
-		}
-		$registrar = Registrar::getInstance($noBase);
-		if (!$registrar->hasFeature('is_content')) {
-			return;
-		}
-
-
-		$settings = $registrar->getIntegrationConfig($this->service_name);
-		if (!$settings) {
-			return;
-		}
-
-		// Similar sync logic as handleSavePost but for terms
-		$this->handleTheTermSave($term_id, $taxonomy, $update, $settings);
-	}
-
-	protected function handleTheTermSave($term_id, $taxonomy, $update, $settings) {
-
-	}
 
 
 	/*******************************************************************
@@ -1494,183 +520,6 @@
 	 *******************************************************************/
 
 	/**
-	 * Get API URL for endpoint
-	 */
-	protected function getApiUrl(string $endpoint, ?string $baseKey = null): string|false
-	{
-		if ($this->hasOAuth && in_array($endpoint, $this->oauth)) {
-			return $endpoint;
-		}
-		if (is_array($this->apiBase)) {
-			if ($baseKey && array_key_exists($baseKey, $this->apiBase)) {
-				$base = $this->apiBase[$baseKey];
-			} else {
-				$base = ($this->apiBase['base'] ?? reset($this->apiBase));
-			}
-		} else {
-			$base = $this->apiBase;
-		}
-
-		if (!$base || $base === '') {
-			$this->logError('API base URL not configured for {$this->>service_name}');
-			return false;
-		}
-
-		// Handle named endpoints
-		if (!$this->isValidEndpoint($endpoint)) {
-			$this->logError("{$endpoint} is not a valid endpoint for {$this->service_name}");
-			return false;
-		}
-
-		// Build full URL
-		$base = rtrim($base, '/');
-		$endpoint = ltrim($endpoint, '/');
-
-		return "{$base}/{$endpoint}";
-	}
-
-	/**
-	 * Check if an endpoint is valid, supporting both exact matches and patterns
-	 *
-	 * @param string $endpoint
-	 * @return bool
-	 */
-	protected function isValidEndpoint(string $endpoint): bool
-	{
-		// Remove query parameters for validation
-		$endpointPath = parse_url($endpoint, PHP_URL_PATH);
-		if ($endpointPath === false) {
-			$endpointPath = $endpoint;
-		}
-
-		foreach ($this->apiEndpoints as $pattern) {
-			// Check for exact match first
-			if ($endpointPath === $pattern) {
-				return true;
-			}
-
-			if (str_starts_with($endpointPath, $pattern)) {
-				return true;
-			}
-
-			// Check if pattern contains wildcards (indicated by square brackets)
-			if (strpos($pattern, '[') !== false) {
-				// Convert the pattern to a regex
-				$regexPattern = '#^' . str_replace(['[^/]+'], ['[^/]+'], $pattern) . '(?:\?.*)?$#';
-				if (preg_match($regexPattern, $endpoint)) {
-					return true;
-				}
-			}
-		}
-
-		return false;
-	}
-
-
-
-	/**
-	 * Categorize API errors for better tracking
-	 */
-	protected function categorizeApiError(int $code): string
-	{
-		return match(true) {
-			$code >= 400 && $code < 404 => 'client_error',
-			$code === 404 => 'not_found',
-			$code === 401 => 'authentication',
-			$code === 403 => 'authorization',
-			$code === 429 => 'rate_limit',
-			$code >= 500 && $code < 600 => 'server_error',
-			default => 'unknown'
-		};
-	}
-
-	/**
-	 * Determine error severity based on HTTP code
-	 */
-	protected function getErrorSeverity(int $code): string
-	{
-		return match(true) {
-			$code === 429 => 'warning', // Rate limiting
-			$code >= 400 && $code < 500 => 'error', // Client errors
-			$code >= 500 => 'critical', // Server errors
-			default => 'error'
-		};
-	}
-
-	/**
-	 * Extract error details from response
-	 */
-	protected function extractErrorDetails($decoded, string $body): array
-	{
-		$details = [
-			'message' => 'Unknown error',
-			'code' => null,
-			'details' => null
-		];
-
-		if ($decoded && isset($decoded['error'])) {
-			if (is_array($decoded['error'])) {
-				$details['message'] = $decoded['error']['message'] ?? json_encode($decoded['error']);
-				$details['code'] = $decoded['error']['code'] ?? null;
-				$details['details'] = $decoded['error']['details'] ?? null;
-			} else {
-				$details['message'] = $decoded['error'];
-			}
-		} elseif ($decoded && isset($decoded['message'])) {
-			$details['message'] = $decoded['message'];
-		} elseif (!empty($body)) {
-			$details['message'] = $body;
-		}
-
-		return $details;
-	}
-
-	/**
-	 * Extract rate limit information from response
-	 */
-	protected function extractRateLimitInfo($decoded, string $body): array
-	{
-		$info = [
-			'retry_after' => null,
-			'limit' => null,
-			'remaining' => null,
-			'reset' => null
-		];
-
-		// Try to extract from decoded response
-		if ($decoded) {
-			$info['retry_after'] = $decoded['retry_after'] ?? $decoded['retry-after'] ?? null;
-			$info['limit'] = $decoded['x-rate-limit-limit'] ?? null;
-			$info['remaining'] = $decoded['x-rate-limit-remaining'] ?? null;
-			$info['reset'] = $decoded['x-rate-limit-reset'] ?? null;
-		}
-
-		return $info;
-	}
-
-	/**
-	 * Update error statistics
-	 */
-	protected function updateErrorStats(int $code, string $endpoint): void
-	{
-		$this->error_stats['total_errors']++;
-		$this->error_stats['consecutive_errors']++;
-
-		// Track error types
-		$error_type = $this->categorizeApiError($code);
-		if (!isset($this->error_stats['error_types'][$error_type])) {
-			$this->error_stats['error_types'][$error_type] = 0;
-		}
-		$this->error_stats['error_types'][$error_type]++;
-
-		// Save stats to cache
-		$this->saveErrorStats();
-	}
-
-
-
-
-	/**
 	 * Get service name
 	 */
 	public function getServiceName(): string
@@ -1697,24 +546,6 @@
 		return static::getInstance()::$hasExtraOptions;
 	}
 
-	/*********************************************************************
-		RENDERING
-	 *********************************************************************/
-//	public function renderAdditionalOptions()
-//	{
-//		//Default: nothing.
-//	}
-
-	/**
-	 * Render additional action buttons (optional)
-	 * Override in integration classes that need extra actions
-	 */
-//	public function renderAdditionalActions(): void
-//	{
-//		// Default: no additional actions
-//		// Override in extensions for service-specific actions
-//	}
-
 	/**
 	 * Get service description (optional)
 	 * Override in integration classes for custom descriptions
@@ -1746,236 +577,6 @@
 	}
 
 
-
-
-
-
-	public function renderConnection(bool $return = false):string
-	{
-
-		if ($this->userID && !JVB()->userCanConnect($this->service_name, $this->userID)) {
-			return '';
-		}
-
-		$meta = Meta::forOptions($this->userID.'_integrations');
-		$is_connected = $this->isSetUp();
-		$credentials = $this->getCredentials();
-
-		$admin_only = $this->isOAuthService ? [
-			'client_id',
-			'client_secret',
-		] : [];
-
-		ob_start();
-		?>
-		<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 x-btw">
-				<h3><?php echo esc_html($this->title); ?></h3>
-				<div class="setup">
-					<?php if ($is_connected): ?>
-						<span class="indicator connected">●</span>
-						<span class="text">Set Up</span>
-					<?php else: ?>
-						<span class="indicator disconnected">●</span>
-						<span class="text">Not Set Up</span>
-					<?php endif; ?>
-				</div>
-			</div>
-
-			<?php if ($is_connected && array_key_exists('updated_at', $credentials) && $credentials['updated_at'] > 0): ?>
-				<div class="meta">
-					<small>Last updated: <?php echo human_time_diff($credentials['updated_at']) . ' ago'; ?></small>
-				</div>
-			<?php endif; ?>
-
-			<?php
-			if (!empty($this->instructions)) {
-				?>
-				<details>
-					<summary>
-						Instructions
-					</summary>
-					<ol>
-						<?php
-						foreach ($this->instructions as $instruction) {
-							echo '<li>'.$instruction.'</li>';
-						}
-						?>
-					</ol>
-				</details>
-				<?php
-			}
-?>
-			<details class="initial-setup"<?= $is_connected?'' : ' open'?>>
-				<summary>Initial Setup</summary>
-				<?php
-				foreach ($this->fields as $name => $config) {
-					if ($is_connected && !empty($credentials[$name])) {
-					if (in_array($name, $admin_only) && !current_user_can('manage_options')) {
-						continue;
-					}
-					?>
-					<span class="label"><?=$config['label']?>:</span>
-					<code>
-						<?php
-						if (str_contains($name, 'secret')) {
-							for ($i = 1; $i<=strlen($credentials[$name]) - 8; $i++) {
-								echo '*';
-							}
-							echo substr($credentials[$name], -8);
-						} else {
-							echo $credentials[$name];
-						}
-						?>
-					</code>
-					<?php
-					} else {
-						$config['value'] = $credentials[$name]??'';
-						$config['autocomplete'] = 'off';
-						$config['base'] = $this->service_name.'_';
-						echo Form::render($name, '', $config);
-					}
-				}
-				if ($this->hasWebhooks) {
-					echo $this->renderWebhookUrl();
-				}
-				?>
-			</details>
-			<?php
-
-			if ($this->isOAuthService) {
-				$this->renderConnectedOAuthStatus();
-			}
-
-			?>
-
-			<div class="integration-content">
-
-				<?php
-
-
-
-				if (!empty($this->advanced)) {
-					?>
-					<details>
-						<summary>Advanced Settings</summary>
-						<?php
-						foreach ($this->advanced as $name => $config) {
-							$config['value'] = $credentials[$name]??'';
-							$config['base'] = $this->service_name.'_';
-							$config['autocomplete'] = 'off';
-							Form::render($name,null, $config);
-						}
-						?>
-					</details>
-					<?php
-				}
-				if (!empty($this->defaults)) {
-					?>
-					<a href="<?php echo admin_url('admin.php?page=jvb-integration-' . $this->service_name); ?>"
-					   class="button">
-						More Settings
-					</a>
-					<?php
-				}
-				?>
-			</div>
-			<div class="actions row x-btw wrap">
-				<?php
-				foreach ($this->buttons as $action => $label) {
-					if (!$is_connected && $action !== 'save_credentials') {
-						continue;
-					}
-					$title = $confirm = '';
-					switch ($action) {
-						case 'save_credentials':
-							$title = $label;
-							$label = jvbIcon('floppy-disk');
-							break;
-						case 'clear_credentials':
-							$title = $label;
-							$label = jvbIcon('plugs');
-							$confirm = ' data-confirm="Are you sure you want to delete these credentials?"';
-							break;
-						case 'clear_cache':
-							$title = $label;
-							$label = jvbIcon('arrows-clockwise');
-							break;
-					}
-					$title = $title === '' ? '' : ' title ="'.$title.'"';
-					?>
-					<button type="button" data-action="<?=$action?>"<?=$title?><?=$confirm?>><?=$label?></button>
-					<?php
-				}
-				?>
-			</div>
-		</form>
-		<?php
-		$result = ob_get_clean();
-		if(!$return) {
-			echo $result;
-		}
-		return $result;
-	}
-
-	protected function renderConnectedOAuthStatus(): void
-	{
-		if (!$this->isSetup()) {
-			return;
-		}
-		$credentials = $this->getCredentials();
-		$hasCredentials = $this->hasOAuthCredentials();
-		$returnURL = is_admin() ? admin_url('admin.php?page=jvb-integrations') : (get_the_permalink() ?: home_url());
-		?>
-
-		<details <?= $hasCredentials?' open':''?>>
-			<summary>
-				<?php if ($hasCredentials) { ?>
-					Connected Account
-				<?php } else { ?>
-					<div class="oauth-connect">
-						<a href="<?php echo esc_url($this->getOAuthUrl($returnURL)); ?>"
-						   class="button button-primary jvb-oauth-connect"
-						   data-service="<?php echo esc_attr($this->service_name); ?>">
-							<?php echo jvbIcon($this->icon); ?>
-							Authorize Connection
-						</a>
-					</div>
-				<?php } ?>
-				<div class="connection-status <?= $hasCredentials ? 'connected' : 'disconnected' ?>">
-					<span class="status-indicator">●</span>
-					<span><?= $hasCredentials ? 'Connected' : 'Not Connected' ?></span>
-				</div>
-			</summary>
-			<label>OAuth Redirect URL:</label>
-			<code>
-				<?= $this->getRedirectUri(); ?>
-			</code>
-
-
-		<?php if (!empty($credentials['updated_at'])): ?>
-		<div class="oauth-meta">
-			<small>Token expires: <?php
-				echo isset($credentials['expires_at'])
-					? human_time_diff($credentials['expires_at'])
-					: 'Never';
-				?></small>
-		</div>
-		<?php endif;
-		// Allow child classes to add service-specific connected UI
-		$this->renderOAuthConnectedOptions();
-		?>
-		</details>
-		<?php
-	}
-
-	protected function renderOAuthConnectedOptions():void
-	{
-
-	}
-
-
 	/**
 	 * Update last tested time
 	 */
@@ -1985,180 +586,6 @@
 		$cred->updateTested($this->service_name, $this->userID);
 	}
 
-	public function handleAdminPost(): void
-	{
-		if (!current_user_can('manage_options')) {
-			wp_die('Insufficient permissions');
-		}
-
-		$service = $this->getServiceName();
-
-		// Verify nonce
-		$nonce_field = 'jvb_integration_nonce_' . $service;
-		$nonce_action = 'jvb_integration_save_' . $service;
-		if (!isset($_POST[$nonce_field]) || !wp_verify_nonce($_POST[$nonce_field], $nonce_action)) {
-			wp_die('Security check failed');
-		}
-
-		// Get the action type
-		$action_type = sanitize_text_field($_POST['action_type'] ?? 'save');
-
-		// Prepare the request in the format handleAjaxRequest expects
-		$_REQUEST['action'] = $action_type;
-		$_REQUEST['service'] = $service;
-
-		// Copy all POST data to REQUEST
-		foreach ($_POST as $key => $value) {
-			$_REQUEST[$key] = $value;
-		}
-
-		// Set up for JSON response capture
-		ob_start();
-
-		try {
-			// Call the existing AJAX handler
-			$this->handleAjaxRequest();
-			$response = ob_get_clean();
-
-			// Parse the JSON response
-			$result = json_decode($response, true);
-
-			if ($result && isset($result['success'])) {
-				if ($result['success']) {
-					$message = $result['message'] ?? ucfirst($service) . ' settings saved successfully!';
-					$this->setAdminNotice($message, 'success');
-				} else {
-					$message = $result['message'] ?? 'Failed to save ' . ucfirst($service) . ' settings.';
-					$this->setAdminNotice($message, 'error');
-				}
-			} else {
-				// If no proper JSON response, check if connection worked
-				if ($action_type === 'test') {
-					$connected = $this->testConnection();
-					$message = $connected ? 'Connection successful!' : 'Connection failed. Please check your credentials.';
-					$this->setAdminNotice($message, $connected ? 'success' : 'error');
-				}
-			}
-		} catch (Exception $e) {
-			ob_end_clean();
-			$this->setAdminNotice('Error: ' . $e->getMessage(), 'error');
-		}
-
-		// Redirect back to integrations page
-		wp_redirect(admin_url('admin.php?page=jvb-integrations'));
-		exit;
-	}
-
-	/**
-	 * Set admin notice using transients for redirect
-	 */
-	protected function setAdminNotice(string $message, string $type = 'info'): void
-	{
-		$notices = get_transient('jvb_admin_notices') ?: [];
-		$notices[] = [
-			'message' => $message,
-			'type' => $type === 'success' ? 'updated' : 'error'
-		];
-		set_transient('jvb_admin_notices', $notices, 30);
-	}
-
-	/**
-	 * Display admin notices from transient
-	 */
-	public static function displayAdminNotices(): void
-	{
-		$notices = get_transient('jvb_admin_notices');
-
-		if ($notices) {
-			foreach ($notices as $notice) {
-				?>
-				<div class="notice notice-<?php echo esc_attr($notice['type']); ?> is-dismissible">
-					<p><?php echo esc_html($notice['message']); ?></p>
-				</div>
-				<?php
-			}
-			delete_transient('jvb_admin_notices');
-		}
-	}
-
-	public function hasDefaults():bool
-	{
-		return !empty($this->defaults);
-	}
-
-	public function renderDefaults():void
-	{
-		$types = $this->enabledContentTypes();
-		if (empty($types)) {
-			return;
-		}
-		$meta = Meta::forOptions($this->userID.'_integrations');
-		?>
-		<form>
-			<h1><?= $this->title?> Defaults:</h1>
-			<p>Find yourself constantly repeating yourself?</p>
-			<p>Set defaults for different content types and <?=$this->title?>!</p>
-			<?php
-			foreach ($this->defaults as $name => $config) {
-				$config['required'] = false;
-
-				$config['base'] = $this->service_name.'_';
-				$config['autocomplete'] = 'off';
-				echo Form::render($name, null, $config);
-			}
-			foreach ($this->syncPostTypes as $type) {
-				$registrar = Registrar::getInstance($type);
-
-				$icon = $registrar->getIcon();
-				$icon = $icon === '' ? jvbDefaultIcon() : $icon;
-				?>
-				<details>
-					<summary><?= jvbIcon($icon) ?><?= $registrar->getSingular()?> Defaults</summary>
-					<?php
-					$fields = new AddIntegrationFields($this->service_name);
-					$fields = $fields->getIntegrationFields();
-					foreach($fields as $name=>$c) {
-						$c['required'] = false;
-						if ($c['type'] === 'number') {
-							$c['type'] = 'text';
-							$c['subtype'] = 'number';
-						}
-						if (array_key_exists('description', $c)) {
-							$c['hint'] = $c['description'];
-							unset($c['description']);
-						}
-						echo Form::render($name, null, $c);
-					}
-					?>
-				</details>
-				<?php
-			}
-			?>
-		</form>
-		<?php
-	}
-
-	public function hasContent():bool
-	{
-		return $this->has_content;
-	}
-	public function getDefaultContentType():string
-	{
-		return $this->defaultContent;
-	}
-
-	public function enabledContentTypes():array
-	{
-		if (!$this->has_content) {
-			return [];
-		}
-
-		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
 	{
 		//If this integration supports webp, we can just send the original image id
@@ -2187,11 +614,6 @@
 	}
 
 
-	public function getAllowedContent():array
-	{
-		return $this->allowedContent;
-	}
-
 	/**
 	 * Used by JVBase\registrar\helpers\AddIntegrationFields.php
 	 * @return array
@@ -2206,18 +628,4 @@
 		return $this->icon;
 	}
 
-
-
-	public function canBatchUpdate():bool
-	{
-		return $this->hasBatchUpdate;
-	}
-	public function canBatchCreate():bool
-	{
-		return $this->hasBatchCreate;
-	}
-	public function canBatchDelete():bool
-	{
-		return $this->hasBatchDelete;
-	}
 }
diff --git a/inc/integrations/OAuth.php b/inc/integrations/OAuth.php
index 5f8085f..3bc410c 100644
--- a/inc/integrations/OAuth.php
+++ b/inc/integrations/OAuth.php
@@ -12,6 +12,7 @@
 {
 	use _Base, Admin;
 	protected bool $hasOAuth = true;
+	protected OAuthURLs $oauth;
 	/**
 	 * Child classes must set this (e.g. 'https://api.example.com/v1/')
 	 */
@@ -21,8 +22,25 @@
 	protected array $oathScopes = [];
 
 
-	abstract protected function getOAuthRequestHeaders(): array;
-	abstract protected function handleOAuthResponse(array|WP_Error $response, string $method, string $endpoint): array;
+	/**
+	 * @return array Defaults to the base getRequestHeaders, but can optionally be overridden
+	 */
+	protected function getOAuthRequestHeaders(): array
+	{
+		return $this->getRequestHeaders();
+	}
+
+	/**
+	 * Default to the base handleResponse method, but extensions can modify accordingly
+	 * @param array|WP_Error $response
+	 * @param string $method
+	 * @param string $endpoint
+	 * @return array
+	 */
+	protected function handleOAuthResponse(array|WP_Error $response, string $method, string $endpoint): array
+	{
+		return $this->handleResponse($response, $method, $endpoint);
+	}
 	abstract protected function refreshAccessToken(OAuthCredentials $credentials): array;
 
 	function addOAuthActions():void
@@ -39,16 +57,68 @@
 				'icon'		=> 'plugs-disconnected',
 				'action'	=> [$this, 'handleOAuthDisconnect']
 			],
-			[
-				'name'		=> 'Refresh Token',
-				'icon'		=> 'arrows-clockwise',
-				'action'	=> [$this, 'handleOAuthRefreshToken']
-			]
+//			[
+//				'name'		=> 'Refresh Token',
+//				'icon'		=> 'arrows-clockwise',
+//				'action'	=> [$this, 'handleOAuthRefreshToken']
+//			]
 		];
 		foreach ($actions as $a) {
 			$this->addAction($a['name'], $a['action'], $a['slug']??null, $a['icon']??null, $a['label']??null);
 		}
 	}
+		public function handleOAuthConnect():array
+		{
+			if (!$this->hasOAuth) {
+				return $this->response(false, 'Not an OAuth service');
+			}
+			$url = $this->getOAuthUrl();
+			if ($url) {
+				return [
+					'success'	=> true,
+					'redirect'	=> $url,
+				];
+			}
+			return $this->response(false, 'Failed to generate OAuth URL');
+		}
+
+		public function handleOAuthDisconnect():array
+		{
+			$revoked = $this->revokeOAuth();
+			return $this->response($revoked, $revoked ?
+			'Successfully disconnected from '.$this->title :
+			'Failed to disconnect from '.$this->title);
+		}
+
+		public function revokeOAuth():bool
+		{
+			if (!$this->hasOAuth) {
+				return false;
+			}
+			if (empty($this->oauth['revoke'])) {
+				error_log('No revoke url set for '.$this->service_name);
+				return false;
+			}
+			$credentials = $this->loadCredentials();
+			if (empty($credentials['access_token'])) {
+				error_log('Could not send revoke request, no access_token for '.$this->service_name);
+			} else {
+				$response = $this->postOAuthRequest($this->oauth['revoke'], [
+					'body'	=> [
+						'token'	=> $credentials['access_token']
+					]
+				]);
+				$empty = new OAuthCredentials([]);
+				foreach ($empty->toArray() as $key => $value) {
+					if (array_key_exists($key, $credentials)) {
+						unset($credentials[$key]);
+					}
+				}
+
+				return Auth::getInstance()->storeCredentials($this->service_name, $credentials, $this->userID);
+			}
+			return false;
+		}
 
 	protected function registerOAuthCallbacks():void
 	{
@@ -105,10 +175,6 @@
 			return $this->handleOAuthResponse($error, $method, $endpoint);
 		}
 
-		if (!empty($this->requiredScopes) && !$credentials->hasScopes($this->requiredScopes)) {
-			$error = new WP_Error('insufficient_scope', "Missing required scopes for {$this->service_name}");
-			return $this->handleOAuthResponse($error, $method, $endpoint);
-		}
 
 		$url = rtrim($this->baseOAuthUrl, '/') . '/' . ltrim($endpoint, '/');
 
@@ -300,7 +366,7 @@
 			return '';
 		}
 		$credentials = $this->loadCredentials();
-		if (empty($credentials['authorize'])) {
+		if (empty($this->oauth['authorize'])) {
 			$this->logError('getOAuthUrl', 'OAuth authorize URL not configured');
 			return '';
 		}
@@ -308,7 +374,7 @@
 			'client_id'		=> $credentials['client_id']??$credentials['application_id']??$credentials['app_id']??'',
 			'redirect_url'	=> $this->getRedirectUri(),
 			'response_type'	=> 'code',
-			'scope'			=> implode(' ', $credentials['scopes']??[])
+			'scope'			=> implode(' ', $this->oauthScopes??[])
 		];
 		$state_key = wp_generate_password(32, false);
 		$user_id = $this->userID??0;
@@ -338,7 +404,7 @@
 			$params = $this->addOAuthParams($params);
 		}
 
-		return $credentials['authorize'] . '?' . http_build_query($params);
+		return $this->oauth['authorize'] . '?' . http_build_query($params);
 	}
 
 	protected function getRedirectUri():string
diff --git a/inc/integrations/OAuthCredentials.php b/inc/integrations/OAuthCredentials.php
index 4664ab8..ff59d7d 100644
--- a/inc/integrations/OAuthCredentials.php
+++ b/inc/integrations/OAuthCredentials.php
@@ -10,6 +10,7 @@
 	public string $access_token;
 	public int $expires_at;
 	public ?string $refresh_token;
+	public ?string $revoke;
 	public array $scopes;
 
 	public function __construct(array $data)
@@ -41,7 +42,7 @@
 			'access_token'	=> $this->access_token,
 			'expires_at'	=> $this->expires_at,
 			'refresh_token'	=> $this->refresh_token,
-			'scopes'		=> $this->scopes,
+//			'scopes'		=> $this->scopes,
 		];
 	}
 }
diff --git a/inc/integrations/OAuthURLs.php b/inc/integrations/OAuthURLs.php
new file mode 100644
index 0000000..13a361a
--- /dev/null
+++ b/inc/integrations/OAuthURLs.php
@@ -0,0 +1,26 @@
+<?php
+namespace JVBase\integrations;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class OAuthURLs {
+	protected string $authorize;
+	protected string $token;
+	protected string $revoke;
+	public function __construct(string $authorize, string $token, string $revoke) {
+		$this->authorize = filter_var($authorize, FILTER_SANITIZE_URL);
+		$this->token = filter_var($token, FILTER_SANITIZE_URL);
+		$this->revoke = filter_var($revoke, FILTER_SANITIZE_URL);
+	}
+	public function getAuthorize():string {
+		return $this->authorize;
+	}
+	public function getToken():string {
+		return $this->token;
+	}
+	public function getRevoke():string{
+		return $this->revoke;
+	}
+}
diff --git a/inc/integrations/SyncDefaults.php b/inc/integrations/SyncDefaults.php
index 383aa7b..4c9ad2b 100644
--- a/inc/integrations/SyncDefaults.php
+++ b/inc/integrations/SyncDefaults.php
@@ -1,6 +1,12 @@
 <?php
 namespace JVBase\integrations;
 
+use JVBase\base\Options;
+use JVBase\meta\Form;
+use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
+use JVBase\ui\Tabs;
+
 if (!defined('ABSPATH')) {
 	exit;
 }
@@ -9,5 +15,170 @@
  * Handles the getting and setting of default values for service fields.
  */
 trait SyncDefaults {
+	use _Base;
+	protected array $allContent;
+	protected array $enabledTaxonomies;
+	protected array $enabledContent;
+	protected array $enabledUsers;
+	public function enabledContentTypes():array
+	{
+		//Build an array of post type/taxonomy slugs => integration content type
+		if (!isset($this->allContent)) {
+			$content = Registrar::withIntegration($this->service_name);
+			$this->allContent = [];
+			foreach ($content as $c) {
+				$registrar = Registrar::getInstance($c);
+				if ($registrar){
+					$this->allContent[$c] = $registrar->getIntegration($this->service_name)->getContentType();
+				}
+			}
+		}
+		return $this->allContent;
+	}
+	public function enabledContent():array
+	{
+		if (!isset($this->enabledContent)) {
+			$content = Registrar::withIntegration($this->service_name, 'post');
+			$this->enabledContent = [];
+			foreach ($content as $c) {
+				$registrar = Registrar::getInstance($c);
+				if ($registrar){
+					$this->enabledContent[$c] = $registrar->getIntegration($this->service_name)->getContentType();
+				}
+			}
+		}
+		return $this->enabledContent;
+	}
+	public function enabledTaxonomies():array
+	{
+		if (!isset($this->enabledTaxonomies)) {
+			$content = Registrar::withIntegration($this->service_name, 'term');
+			$this->enabledTaxonomies = [];
+			foreach ($content as $c) {
+				$registrar = Registrar::getInstance($c);
+				if ($registrar){
+					$this->enabledTaxonomies[$c] = $registrar->getIntegration($this->service_name)->getContentType();
+				}
+			}
+		}
+		return $this->enabledTaxonomies;
+	}
+	public function enabledUsers():array
+	{
+		if (!isset($this->enabledUsers)) {
+			$content = Registrar::withIntegration($this->service_name, 'user');
+			$this->enabledUsers = [];
+			foreach ($content as $c) {
+				$registrar = Registrar::getInstance($c);
+				if ($registrar){
+					$this->enabledUsers[$c] = $registrar->getIntegration($this->service_name)->getContentType();
+				}
+			}
+		}
+		return $this->enabledUsers;
+	}
 
+	public function renderDefaults():void
+	{
+		$tabs = new Tabs();
+
+		$types = [
+			'enabledContent' => [
+				'title'	=> 'Content',
+				'slug'	=> 'content',
+				'icon'	=> 'note-blank',
+				'description'	=> 'Set default values for the meta for different content types here.'
+			],
+			'enabledTaxonomies' => [
+				'title'	=> 'Taxonomies',
+				'slug'	=> 'taxonomies',
+				'icon'	=> 'folder',
+				'description'	=> 'Set default values for the meta for different taxonomies here.'
+			],
+			'enabledUsers' => [
+				'title'	=> 'Users',
+				'slug'	=> 'users',
+				'icon'	=> 'user',
+				'description'	=> 'Set default values for the meta for different user types here.'
+			]
+		];
+
+		foreach ($types as $method => $options) {
+			$registered = $this->$method();
+			if (empty($registered)) {
+				continue;
+			}
+			$role = !is_null($this->userID) ? jvbUserRole($this->userID) : '';
+			$userRegistrar = null;
+			if (!empty($role)) {
+				$userRegistrar = Registrar::getInstance($role);
+			}
+			$allowed = is_null($this->userID) ?
+				array_keys($registered) :
+				array_filter(array_keys($registered), function ($type) use ($userRegistrar) {
+					return in_array($type, $userRegistrar->getCreatable());
+				});
+			if (empty($allowed)) {
+				continue;
+			}
+
+			$tab = $tabs->addTab($options['slug'])
+				->icon($options['icon'])
+				->title($options['title'])
+				->description($options['description'])
+				->content($this->getContentTypeContent($allowed));
+		}
+		$tabs->render(false);
+	}
+	public function getContentTypeContent(array $content):string
+	{
+		$tabs = new Tabs();
+		foreach ($content as $c) {
+			$registrar = Registrar::getInstance($c);
+			$additionalFields = $registrar->getIntegrationFields($this->service_name);
+			$fields = $additionalFields->getIntegrationFields();
+			if (empty($fields)) {
+				continue;
+			}
+			$form = '';
+			foreach ($fields as $name => $config) {
+				$form .= Form::render(
+					$this->getFieldName($name, $c),
+					$this->getDefaultValue($name, $c, $this->userID),
+					$config
+				);
+			}
+
+			$tab = $tabs->addTab($c)
+				->icon($registrar->getIcon())
+				->title($registrar->getPlural())
+				->description('Set default values for the meta for '.$registrar->getPlural().' here.')
+				->content($form);
+		}
+		return $tabs->render();
+	}
+
+	protected function getFieldName(string $fieldName, string $type):string
+	{
+		return $type.'_defaults_'.$fieldName;
+	}
+
+	public function getDefaultValue(string $fieldName, string $type, ?int $userID = null):mixed
+	{
+		$name = $this->getFieldName($fieldName, $type);
+		if (is_null($userID)) {
+			return Options::get($name);
+		} else {
+			return Meta::forUser($userID)->get($name);
+		}
+	}
+	public function setDefaultMetaValue(string $fieldName, mixed $value, string $type, ?int $userID = null):void
+	{
+		$name = $this->getFieldName($fieldName, $type);
+		if (is_null($userID)) {
+			Options::set($name, $value);
+		} else {
+			Meta::forUser($userID)->set($name, $value);
+		}
+	}
 }
diff --git a/inc/integrations/SyncHelpers.php b/inc/integrations/SyncHelpers.php
index f2d0ca0..778902a 100644
--- a/inc/integrations/SyncHelpers.php
+++ b/inc/integrations/SyncHelpers.php
@@ -1,6 +1,9 @@
 <?php
 namespace JVBase\integrations;
 
+use JVBase\managers\queue\executors\IntegrationExecutor;
+use JVBase\managers\queue\mergers\DefaultMerger;
+use JVBase\managers\queue\TypeConfig;
 use JVBase\meta\Meta;
 use WP_Query;
 
@@ -20,6 +23,7 @@
 	protected static string $syncCustomer;
 	protected static string $import;
 
+
 	protected function setQueueTypes():void
 	{
 		self::$syncTo = $this->service_name.'_sync_to';
@@ -141,4 +145,47 @@
 		]);
 		return is_wp_error($get) || empty($get) ? false : [$get[0], 'user'];
 	}
+
+	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.
+	}
+
+	/**
+	 * @return void Sets the $this->contentTypes
+	 */
+	abstract protected function setContentTypes():void;
 }
diff --git a/inc/integrations/SyncTo.php b/inc/integrations/SyncTo.php
index dd1e9ca..223a7db 100644
--- a/inc/integrations/SyncTo.php
+++ b/inc/integrations/SyncTo.php
@@ -2,14 +2,20 @@
 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;
 }
 
 trait SyncTo {
-	use SyncHelpers;
+	use SyncHelpers, UserConnection;
 	/**
 	 * Must be defined according to how each service needs it to be
 	 * @param int $itemID
@@ -17,10 +23,7 @@
 	 * @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
@@ -35,7 +38,7 @@
 			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(),
@@ -74,18 +77,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
@@ -198,7 +195,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
@@ -307,4 +304,434 @@
 		}
 		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 [];
+		}
+
+		$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);
+	}
+	/****************************************************************
+	 * POST SYNC
+	****************************************************************/
+	public function addSavePost():void
+	{
+		if (!has_action('save_post', [$this, 'handleSavePost'])) {
+			add_action('save_post', [$this, 'handleSavePost'], 20, 3);
+		}
+	}
+	public function removeSavePost():void
+	{
+		remove_action('save_post', [$this, 'handleSavePost'], 20, 3);
+	}
+	public function handleSavePost(int $postID, WP_Post $post, bool $update):void
+	{
+		if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
+		if (wp_is_post_revision($postID)) return;
+
+		error_log('=== ['.$this->service_name.']::handleSavePost called');
+
+		$postType = jvbNoBase($post->post_type);
+		if (!in_array($postType, $this->syncPostTypes)) {
+			error_log('Not handling save for '.$this->service_name.' because there are no syncPostTypes: '.print_r($this->syncPostTypes, true));
+			return;
+		}
+
+		$registrar = Registrar::getInstance($postType);
+		//Should not happen, as syncPostTypes is defined by Registrar instances
+		if (!$registrar) {
+			return;
+		}
+
+		$settings = $registrar->getIntegrationConfig($this->service_name);
+		if (!$settings) {
+			error_log('Not handling save for '.$this->service_name.' because of no integration config '.print_r($settings, true));
+			return;
+		}
+
+		$fields = $this->getSyncFields($postID, 'post');
+		if (!$fields['share_to_'.$this->service_name]) {
+			error_log('Not handling save for '.$this->service_name.' because of no share_to_'.$this->service_name.' '.print_r($fields, true));
+			return;
+		}
+
+		$isShared = array_key_exists("_{$this->service_name}_item_id", $fields) && !empty($fields["_{$this->service_name}_item_id"]);
+
+		if ($post->post_status !== 'publish' && !$isShared) {
+			error_log('Not handling save for '.$this->service_name.' because post status is not publish, and it is not already shared.');
+			return;
+		}
+
+		if ($isShared && $update && !$fields['_keep_synced_'.$this->service_name]) {
+			error_log('Not handling save for '.$this->service_name.' because it is already shared, and not set to keep synced. ');
+			return;
+		}
+		error_log('==== Sending to integration\'s handleTheSavePost '.$this->service_name.' ====');
+		$this->removeSavePost();
+		$this->handleTheSavePost($postID, $post, $update, $settings);
+		$this->addSavePost();
+	}
+	/**
+	 * Handle post save for syncing
+	 *
+	 * Override to implement custom sync logic when posts are saved.
+	 * Check the $settings array for post type specific configuration.
+	 *
+	 * @param int $postID The post ID
+	 * @param WP_Post $post The post object
+	 * @param bool $update Whether this is an update
+	 * @param array $settings Post type integration settings
+	 * @return void
+	 */
+	protected function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings):void
+	{
+		error_log('==== ['.$this->title.']::handleTheSavePost ====');
+		$this->queueOperation(self::$syncTo, [
+			'posts'   => [$postID],
+			'user'		=> user_can($post->post_author, 'manage_options') ? null : $post->post_author
+		], [
+			'priority' => 'high',
+			'delay'    => 30,
+		]);
+		Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued');
+	}
+
+	public function addDeletePost():void
+	{
+		if (!has_action('before_delete_post', [$this, 'handleDeletePost'])) {
+			add_action('before_delete_post', [$this, 'handleDeletePost'], 20, 3);
+		}
+	}
+	public function removeDeletePost():void
+	{
+		remove_action('before_delete_post', [$this, 'handleSavePost'], 20, 3);
+	}
+	public function handleDeletePost(int $postID):void
+	{
+		if (!$this->canSync['delete']) {
+			return;
+		}
+		$postType = get_post_type($postID);
+		if (!in_array(jvbNoBase($postType), $this->syncPostTypes)) {
+			return;
+		}
+		$fields = $this->getSyncFields($postID, 'post');
+		if (empty($fields["_{$this->service_name}_item_id"])) {
+			return;
+		}
+		$post = get_post($postID);
+		if (!$post) {
+			return;
+		}
+		$userID = $this->determineUserID($post->post_author);
+		if (!$userID) {
+			return;
+		}
+
+		JVB()->queue()->add(
+			self::$deleteFrom,
+			$userID,
+			[
+				'fields'	=> [$postID => $fields],
+				'service'	=> $this->service_name,
+				'type'		=> 'post'
+			]
+		);
+	}
+	/****************************************************************
+	 * TERM SYNC
+	 ****************************************************************/
+	public function addSaveTerm():void
+	{
+		if (empty($this->syncTaxonomies)) {
+			return;
+		}
+		if (!has_action('saved_term', [$this, 'handleSaveTerm'])) {
+			add_action('saved_term', [$this, 'handleSaveTerm'], 20, 3);
+		}
+	}
+	public function removeSaveTerm():void
+	{
+		if (empty($this->syncTaxonomies)) {
+			return;
+		}
+		remove_action('saved_term', [$this, 'handleSaveTerm'], 20, 3);
+	}
+	protected function handleSaveTerm(int $termID, int $tt_id, string $taxonomy, bool $update, array $args):void
+	{
+		$tax = jvbNoBase($taxonomy);
+		if (!in_array($tax, $this->syncTaxonomies)) {
+			return;
+		}
+		$registrar = Registrar::getInstance($tax);
+		if (!$registrar) {
+			return;
+		}
+
+		$settings = $registrar->getIntegrationConfig($this->service_name);
+		if (!$settings) {
+			return;
+		}
+
+		$fields = $this->getSyncFields($termID, 'term');
+		if (!$fields['share_to_'.$this->service_name]) {
+			return;
+		}
+
+		$isShared = array_key_exists("_{$this->service_name}_item_id", $fields) && !empty($fields["_{$this->service_name}_item_id"]);
+
+		if ($isShared && $update && !$fields['_keep_synced_'.$this->service_name]) {
+			return;
+		}
+		$this->removeSaveTerm();
+		$this->handleTheSaveTerm($termID, $update, $taxonomy, $settings);
+		$this->addSaveTerm();
+	}
+
+	/**
+	 * @param int $termID
+	 * @param bool $update
+	 * @param array $settings
+	 * @return void
+	 */
+	protected function handleTheSaveTerm(int $termID, bool $update, string $taxonomy, array $settings):void
+	{
+		error_log('==== ['.$this->title.']::handleTheSaveTerm ====');
+		//TODO: Figure out some sort of permissions for if the user can share this term, particularly for content types
+		//TODO: If this is a content type, it likely has its own integration. This is an edmonton.ink problem, so I'm offloading it for now
+		$this->queueOperation(self::$syncTo, [
+			'terms'   => [$termID],
+			'user'		=> get_current_user_id(),
+		], [
+			'priority' => 'high',
+			'delay'    => 30,
+		]);
+		Meta::forTerm($termID)->set('_'.$this->service_name.'_sync_status', 'queued');
+	}
+
+	public function addDeleteTerm():void
+	{
+		if (!has_action('pre_delete_term', [$this, 'handleDeleteTerm'])) {
+			add_action('pre_delete_term', [$this, 'handleDeleteTerm']);
+		}
+	}
+	public function removeDeleteTerm():void
+	{
+		remove_action('pre_delete_term', [$this, 'handleDeleteTerm']);
+	}
+	public function handleDeleteTerm(int $termID, string $taxonomy):void
+	{
+		if (!$this->canSync['delete']) {
+			return;
+		}
+		$tax = jvbNoBase($taxonomy);
+		if (!in_array($tax, $this->syncTaxonomies)) {
+			return;
+		}
+		$fields = $this->getSyncFields($termID, 'term');
+		if (empty($fields["_{$this->service_name}_item_id"])) {
+			return;
+		}
+		JVB()->queue()->add(
+			self::$deleteFrom,
+			0,
+			[
+				'fields'	=> [$termID => $fields],
+				'service'	=> $this->service_name,
+				'type'		=> 'term'
+			]
+		);
+	}
+
+	/****************************************************************
+	 * USER SYNC
+	 ****************************************************************/
+	public function addSaveUser():void
+	{
+		if (empty($this->syncUsers)) {
+			return;
+		}
+		if (!has_action('profile_update', [$this, 'handleUpdateUser'])) {
+			add_action('profile_update', [$this, 'handleUpdateUser'], 20, 1);
+		}
+		if (!has_action('user_register', [$this, 'handleUpdateUser'])) {
+			add_action('user_register', [$this, 'handleUpdateUser'], 20, 1);
+		}
+	}
+	public function removeSaveUser():void
+	{
+		if (empty($this->syncUsers)) {
+			return;
+		}
+		remove_action('profile_update', [$this, 'handleUpdateUser'], 20);
+		remove_action('user_register', [$this, 'handleUpdateUser'], 20);
+	}
+	protected function handleUpdateUser(int $userID):void
+	{
+		$user = $this->getOrCreateUser($userID);
+		if (!$user) {
+			return;
+		}
+
+		$fields = $this->getSyncFields($userID, 'user');
+		if (!empty($fields["_{$this->service_name}_item_id"])) {
+			return;
+		}
+
+		$this->removeSaveUser();
+		$this->handleTheSaveUser($userID);
+		$this->addSaveTerm();
+	}
+
+	protected function getOrCreateUser(int $userID):string|false
+	{
+		$user = get_userdata($userID);
+		if (!$user || is_wp_error($user)) {
+			return false;
+		}
+		$role = jvbUserRole($userID);
+		if (!in_array(jvbNoBase($role), $this->syncUsers)) {
+			return false;
+		}
+		$fields = $this->getSyncFields($userID, 'user');
+		if (!empty($fields["_{$this->service_name}_item_id"])) {
+			return $fields["_{$this->service_name}_item_id"];
+		}
+
+		$meta = Meta::forUser($userID);
+		$serviceUserID = $this->searchServiceForUser($user->user_email??'');
+		if ($serviceUserID) {
+			$meta->set("{$this->service_name}_item_id", $serviceUserID);
+			return $serviceUserID;
+		}
+
+		$created = $this->createServiceUser($userID, $fields);
+		if ($created) {
+			return $created;
+		}
+		return false;
+	}
+	public function searchServiceForUser(string $email):string|false
+	{
+		if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+			return false;
+		}
+		return $this->handleEmailSearch($email);
+	}
+
+	/**
+	 * Searches for existing user from sanitized email
+	 * @param string $email
+	 * @return string|false The found service's User ID or false on failure
+	 */
+	abstract public function handleEmailSearch(string $email):string|false;
+
+	public function createServiceUser(int $userID, array $fields):string|false
+	{
+		$checked = $this->validateUserFields($userID, $fields);
+		$response = $this->handleCreateUser($checked);
+		if ($response['success']) {
+			$meta = Meta::forUser($userID);
+			$meta->set("{$this->service_name}_item_id", $response['result']['id']);
+		}
+		return $response['success'] ? $response['customer']??false : false;
+	}
+
+	protected function validateUserFields(int $userID, array $fields):array|false
+	{
+		foreach ($fields as $f => $v) {
+			$v = match ($f) {
+				'email' => filter_var($v, FILTER_SANITIZE_EMAIL),
+				'phone' => Sanitizer::sanitizePhone($v),
+				default => sanitize_text_field($v),
+			};
+			if (empty($v) && in_array($f, $this->requiredUserFields())) {
+				return false;
+			}
+			$fields[$f] = $v;
+		}
+		return $fields;
+	}
+	protected function requiredUserFields():array
+	{
+		return [];
+	}
+
+	/**
+	 * @param array $data User Data
+	 * @return array The created user ID or false on failure
+	 */
+		abstract protected function handleCreateUser(array $data):array;
+
+	/**
+	 * @param int $userID
+	 * @return void
+	 */
+	protected function handleTheSaveUser(int $userID):void
+	{
+		error_log('==== ['.$this->title.']::handleTheSaveUser ====');
+		//TODO: This is likely only for stuff like customers.
+		//If we have multiple stores connected, we may have to queue operations to update every connection's customer account if they made an order with that store
+		$role = jvbUserRole($userID);
+		$registrar = Registrar::getInstance($role);
+		if (!$registrar || !$registrar->hasIntegration($this->service_name)) {
+			return;
+		}
+
+		$this->queueOperation(self::$syncTo, [
+			'users'	=> [$userID],
+		]);
+	}
+
+	public function addDeleteUser():void
+	{
+		if (!has_action('delete_user', [$this, 'handleDeleteTerm'])) {
+			add_action('delete_user', [$this, 'handleDeleteTerm']);
+		}
+	}
+	public function removeDeleteUser():void
+	{
+		remove_action('delete_user', [$this, 'handleDeleteTerm']);
+	}
+	public function handleDeleteUser(int $userID):void
+	{
+		if (!$this->canSync['delete']) {
+			return;
+		}
+
+		$fields = $this->getSyncFields($userID, 'user');
+		if (empty($fields["_{$this->service_name}_item_id"])) {
+			return;
+		}
+		JVB()->queue()->add(
+			self::$deleteFrom,
+			0,
+			[
+				'fields'	=> [$userID => $fields],
+				'service'	=> $this->service_name,
+				'type'		=> 'user'
+			]
+		);
+	}
 }
diff --git a/inc/integrations/UserConnection.php b/inc/integrations/UserConnection.php
index f1f61d1..5202e6e 100644
--- a/inc/integrations/UserConnection.php
+++ b/inc/integrations/UserConnection.php
@@ -9,6 +9,7 @@
 }
 
 trait UserConnection {
+	use _Base;
 	protected function determineUser():void
 	{
 		if (current_user_can('manage_options')) {
@@ -26,6 +27,13 @@
 		$this->initializeRateLimiters();
 		return true;
 	}
+	protected function determineUserID(int $userID):bool|int
+	{
+		if (!$this->userCanConnect($userID)) {
+			return false;
+		}
+		return $userID;
+	}
 	protected function clearUser():void
 	{
 		$this->userID = false;
diff --git a/inc/integrations/_Base.php b/inc/integrations/_Base.php
index 2d62970..80e71f8 100644
--- a/inc/integrations/_Base.php
+++ b/inc/integrations/_Base.php
@@ -30,6 +30,7 @@
 	 */
 	protected string $apiVersion;
 	protected Cache $cache;
+	protected ?string $cacheName = null;
 	//Use Trait "Requests" to set this
 	protected bool $hasRequests = false;
 	protected ?RateLimits $requestLimiter = null;
@@ -61,8 +62,36 @@
 	 * Usually used for default field values for syncable services
 	 */
 	public bool $hasExtraOptions = false;
+	public bool $hasBatchCreate = false;
+	public bool $hasBatchUpdate = false;
+	public bool $hasBatchDelete = false;
+	public bool $canCreateOnUpdate = false;
+	/**
+	 * Post Syncing Capabilities
+	 * Define what sync operations this integration supports
+	 */
+	protected array $canSync = [
+		'initial' => false,    // Can share new posts to the service
+		'update' => false,     // Can update already-shared posts
+		'delete' => false,     // Can remove posts from the service
+	];
 
-
+	/**
+	 * @var array // 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 $syncPostTypes = [];
+	/**
+	 * @var array Taxonomies that can be synced (e.g., ['category', 'section']): usually built by Registrar.php if the integration name exists as a key in [ 'integrations' => []]
+	 */
+	protected array $syncTaxonomies = [];
+	/**
+	 * @var array User roles that can be synced (e.g., ['customer']): usually built by Registrar.php if the integration name exists as a key in [ 'integrations' => []]
+	 */
+	protected array $syncUsers = [];
+	/**
+	 * @var array Integration's specific content types. Set by child classes' setContentTypes
+	 */
+	protected array $contentTypes = [];
 
 
 	/**
@@ -107,4 +136,32 @@
 		}
 		return jvbDashIcon($icon);
 	}
+
+	protected function initializeRateLimiters():void
+	{
+		$key = $this->service_name;
+		if (!is_null($this->userID)) {
+			$key .= '_'.$this->userID;
+		}
+
+		if ($this->hasRequests && !isset($this->requestLimiter) && !is_null($this->oauthLimiter)) {
+			$this->requestLimiter = new RateLimits($key);
+		}
+		if ($this->hasOAuth && !isset($this->oauthLimiter) && !is_null($this->oauthLimiter)) {
+			$key .= '_oauth';
+			$this->oauthLimiter = new RateLimits($key, ['s' => 1,'m' => 10, 'h' => 100]);
+		}
+	}
+
+	public function response(bool $success, string $message, array $data = []):array
+	{
+		$response = [
+			'success'	=> $success,
+			'message'	=> $message
+		];
+		if (!empty($data)) {
+			$response['data'] = $data;
+		}
+		return $response;
+	}
 }
diff --git a/inc/integrations/_setup.php b/inc/integrations/_setup.php
index 9739293..7b0179c 100644
--- a/inc/integrations/_setup.php
+++ b/inc/integrations/_setup.php
@@ -1,11 +1,23 @@
 <?php
 use JVBase\base\Site;
+require(JVB_DIR . '/inc/integrations/_Base.php');
 require(JVB_DIR . '/inc/integrations/Admin.php');
+	require(JVB_DIR . '/inc/integrations/AdminActionResult.php');
+	require(JVB_DIR . '/inc/integrations/AdminActions.php');
 require(JVB_DIR . '/inc/integrations/Ajax.php');
 require(JVB_DIR . '/inc/integrations/Auth.php');
+require(JVB_DIR . '/inc/integrations/Field.php');
 require(JVB_DIR . '/inc/integrations/OAuth.php');
+	require(JVB_DIR . '/inc/integrations/OAuthCredentials.php');
+	require(JVB_DIR . '/inc/integrations/OAuthURLs.php');
 require(JVB_DIR . '/inc/integrations/RateLimits.php');
 require(JVB_DIR . '/inc/integrations/Requests.php');
+require(JVB_DIR . '/inc/integrations/SyncDefaults.php');
+	require(JVB_DIR . '/inc/integrations/SyncFrom.php');
+	require(JVB_DIR . '/inc/integrations/SyncHelpers.php');
+	require(JVB_DIR . '/inc/integrations/SyncTo.php');
+require(JVB_DIR . '/inc/integrations/UserConnection.php');
+require(JVB_DIR . '/inc/integrations/Webhooks.php');
 require(JVB_DIR . '/inc/integrations/Integrations.php');
 
 if (Site::hasIntegration('maps')) {
diff --git a/inc/integrations/services/BlueSkyOld.php b/inc/integrations/services/BlueSkyOld.php
new file mode 100644
index 0000000..f39e341
--- /dev/null
+++ b/inc/integrations/services/BlueSkyOld.php
@@ -0,0 +1,411 @@
+<?php
+namespace JVBase\integrations\services;
+use Exception;
+use JVBase\integrations\Integrations;
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class BlueSkyOld extends Integrations
+{
+	protected string $api_base = 'https://bsky.social/xrpc/';
+	protected int $max_description_length = 300;
+	protected ?string $access_token = null;
+	protected ?string $did = null;
+
+	protected static array $instances = [];
+	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 self($userID);
+		}
+		return self::$instances[$key];
+	}
+
+	protected function __construct(?int $user_id = null) {
+		$this->service_name = 'bluesky';
+		$this->title = 'BlueSky';
+		$this->icon = 'fediverse-logo';
+		$this->canSync = [
+			'initial' 	=> true,
+			'schedule'	=> true,
+		];
+
+		$this->fields = [
+			'handle'	=> [
+				'type'	=> 'text',
+				'placeholder'=> 'your-handle.bsky.social',
+				'required'	=> true,
+				'label'	=> 'Handle',
+			],
+			'password'	=> [
+				'type'	=> 'text',
+				'subtype'=>'password',
+				'label'	=> 'Application Password',
+				'required'	=> true,
+				'hint'	=> '<strong>Security:</strong> Generate an App Password in your BlueSky settings for better security.'
+			]
+		];
+		parent::__construct($user_id);
+	}
+	protected function initialize(): void
+	{
+	}
+
+
+	/**
+	 * Setup BlueSky client with credentials
+	 */
+	protected function setupClient(?int $user_id = null): bool
+	{
+		$creds = $this->credentials;
+
+		if (empty($creds['identifier']) || empty($creds['password'])) {
+			return false;
+		}
+
+		try {
+			$session = $this->createSession($creds['identifier'], $creds['password']);
+
+			if ($session) {
+				$this->access_token = $session['accessJwt'];
+				$this->did = $session['did'];
+				return true;
+			}
+		} catch (Exception $e) {
+			$this->handleError($e);
+		}
+
+		return false;
+	}
+
+	/**
+	 * Create BlueSky session
+	 */
+	private function createSession(string $identifier, string $password): ?array
+	{
+		$response = wp_remote_post($this->api_base . 'com.atproto.server.createSession', [
+			'headers' => [
+				'Content-Type' => 'application/json',
+			],
+			'body' => json_encode([
+				'identifier' => $identifier,
+				'password' => $password
+			]),
+			'timeout' => 30
+		]);
+
+		if (is_wp_error($response)) {
+			throw new Exception('Failed to connect to BlueSky: ' . $response->get_error_message());
+		}
+
+		$body = json_decode(wp_remote_retrieve_body($response), true);
+		$status_code = wp_remote_retrieve_response_code($response);
+
+		if ($status_code !== 200) {
+			throw new Exception('BlueSky authentication failed: ' . ($body['message'] ?? 'Unknown error'));
+		}
+
+		return $body;
+	}
+
+	/**
+	 * Publish a post to BlueSky
+	 */
+	public function publishPost(array $content, ?int $user_id = null): array
+	{
+		try {
+			// Validate content
+			$errors = $this->validateContent($content);
+			if (!empty($errors)) {
+				return [
+					'success' => false,
+					'error' => implode(', ', $errors)
+				];
+			}
+
+			// Setup client for this request
+			if (!$this->setupClient($user_id)) {
+				return [
+					'success' => false,
+					'error' => 'Failed to authenticate with BlueSky'
+				];
+			}
+
+			// Check rate limiting
+			if (!$this->checkRateLimit()) {
+				return [
+					'success' => false,
+					'error' => 'Rate limit exceeded'
+				];
+			}
+
+			// Prepare post data
+			$post_data = [
+				'repo' => $this->did,
+				'collection' => 'app.bsky.feed.post',
+				'record' => [
+					'text' => $this->prepareDescription($content['description'] ?? ''),
+					'createdAt' => date('c'),
+					'$type' => 'app.bsky.feed.post'
+				]
+			];
+
+			// Add link if post URL provided
+			if (!empty($content['post_url'])) {
+				$post_data['record']['text'] .= "\n\n" . $content['post_url'];
+			}
+
+			// Upload and attach image if provided
+			if (!empty($content['image_path']) && file_exists($content['image_path'])) {
+				$image_result = $this->uploadImage($content['image_path']);
+
+				if ($image_result['success']) {
+					$post_data['record']['embed'] = [
+						'$type' => 'app.bsky.embed.images',
+						'images' => [
+							[
+								'alt' => $content['title'] ?? 'Image',
+								'image' => $image_result['blob']
+							]
+						]
+					];
+				}
+			}
+
+			// Post to BlueSky
+			$response = $this->makeApiRequest('com.atproto.repo.createRecord', $post_data);
+
+			if ($response['success']) {
+				$this->log("Successfully posted to BlueSky", 'info');
+
+				return [
+					'success' => true,
+					'platform_id' => $response['data']['uri'] ?? null,
+					'post_url' => $this->getPostUrl($response['data']['uri'] ?? ''),
+					'data' => $response['data']
+				];
+			}
+
+			return [
+				'success' => false,
+				'error' => $response['error'] ?? 'Unknown error'
+			];
+
+		} catch (Exception $e) {
+			$this->handleError($e);
+			return [
+				'success' => false,
+				'error' => $e->getMessage()
+			];
+		}
+	}
+
+	/**
+	 * Validate content before posting
+	 */
+	protected function validateContent(array $content): array
+	{
+		$errors = [];
+
+		// Check description length
+		if (isset($content['description']) && strlen($content['description']) > $this->max_description_length) {
+			$errors[] = "Description exceeds {$this->max_description_length} characters";
+		}
+
+		// Check image if provided
+		if (isset($content['image_path']) && $content['image_path']) {
+			if (!file_exists($content['image_path'])) {
+				$errors[] = "Image file does not exist";
+			} else {
+				$mime_type = mime_content_type($content['image_path']);
+				$supported_types = ['image/jpeg', 'image/png', 'image/gif'];
+
+				if (!in_array($mime_type, $supported_types)) {
+					$errors[] = "Unsupported image type: {$mime_type}";
+				}
+
+				$file_size = filesize($content['image_path']);
+				$max_size = 5242880; // 5MB
+				if ($file_size > $max_size) {
+					$errors[] = "Image too large: " . round($file_size / 1048576, 2) . "MB (max: 5MB)";
+				}
+			}
+		}
+
+		return $errors;
+	}
+
+	/**
+	 * Prepare description with length limits
+	 */
+	protected function prepareDescription(string $description): string
+	{
+		if (strlen($description) <= $this->max_description_length) {
+			return $description;
+		}
+
+		// Truncate intelligently at word boundaries
+		$truncated = substr($description, 0, $this->max_description_length - 3);
+		$last_space = strrpos($truncated, ' ');
+
+		if ($last_space !== false) {
+			$truncated = substr($truncated, 0, $last_space);
+		}
+
+		return $truncated . '...';
+	}
+
+	/**
+	 * Upload image to BlueSky
+	 */
+	protected function uploadImage(string $image_path): array
+	{
+		try {
+			$mime_type = mime_content_type($image_path);
+			$image_data = file_get_contents($image_path);
+
+			$response = wp_remote_post($this->api_base . 'com.atproto.repo.uploadBlob', [
+				'headers' => [
+					'Authorization' => 'Bearer ' . $this->access_token,
+					'Content-Type' => $mime_type,
+				],
+				'body' => $image_data,
+				'timeout' => 60
+			]);
+
+			if (is_wp_error($response)) {
+				throw new Exception('Failed to upload image: ' . $response->get_error_message());
+			}
+
+			$body = json_decode(wp_remote_retrieve_body($response), true);
+			$status_code = wp_remote_retrieve_response_code($response);
+
+			if ($status_code === 200 && isset($body['blob'])) {
+				return [
+					'success' => true,
+					'blob' => $body['blob']
+				];
+			}
+
+			return [
+				'success' => false,
+				'error' => $body['message'] ?? 'Image upload failed'
+			];
+
+		} catch (Exception $e) {
+			return [
+				'success' => false,
+				'error' => $e->getMessage()
+			];
+		}
+	}
+
+	/**
+	 * Make API request to BlueSky
+	 */
+	private function makeApiRequest(string $endpoint, array $data): array
+	{
+		$response = wp_remote_post($this->api_base . $endpoint, [
+			'headers' => [
+				'Authorization' => 'Bearer ' . $this->access_token,
+				'Content-Type' => 'application/json',
+			],
+			'body' => json_encode($data),
+			'timeout' => 30
+		]);
+
+		if (is_wp_error($response)) {
+			return [
+				'success' => false,
+				'error' => $response->get_error_message()
+			];
+		}
+
+		$body = json_decode(wp_remote_retrieve_body($response), true);
+		$status_code = wp_remote_retrieve_response_code($response);
+
+		if ($status_code === 200) {
+			return [
+				'success' => true,
+				'data' => $body
+			];
+		}
+
+		return [
+			'success' => false,
+			'error' => $body['message'] ?? "HTTP {$status_code} error"
+		];
+	}
+
+	/**
+	 * Get post URL from BlueSky URI
+	 */
+	private function getPostUrl(string $uri): string
+	{
+		if (preg_match('/at:\/\/([^\/]+)\/app\.bsky\.feed\.post\/(.+)/', $uri, $matches)) {
+			$did = $matches[1];
+			$rkey = $matches[2];
+			return "https://bsky.app/profile/{$did}/post/{$rkey}";
+		}
+
+		return '';
+	}
+
+	/**
+	 * Disconnect user from this platform
+	 */
+	public function disconnectUser(int $user_id): bool
+	{
+		$key = "user_{$user_id}_{$this->service_name}";
+		$credentials_manager = Auth::getInstance();
+		return $credentials_manager->deleteCredentials($key);
+	}
+
+	/**
+	 * Get display name for this platform
+	 */
+	public function getDisplayName(): string
+	{
+		return 'BlueSky';
+	}
+
+	/**
+	 * Get icon name for this platform
+	 */
+	public function getIconName(): string
+	{
+		return 'fediverse'; // Using existing fediverse icon
+	}
+
+	/**
+	 * Get supported post types
+	 */
+	public function getSupportedPostTypes(): array
+	{
+		return ['text', 'image', 'link'];
+	}
+
+
+	public function getServiceDescription(): string
+	{
+		return "Cross-post content to the BlueSky social network.";
+	}
+
+	protected function getRequestHeaders(): array
+	{
+		// TODO: Implement getRequestHeaders() method.
+		return [];
+	}
+
+	protected function getApiUrl(string $endpoint, ?string $baseUrl = null): string
+	{
+		return 'https://bsky.social/xrpc/';
+	}
+
+	protected function processIntegrationAction(string $action, array $data):\WP_Error|array
+	{
+		return [];
+	}
+}
diff --git a/inc/integrations/services/CloudflareOld.php b/inc/integrations/services/CloudflareOld.php
new file mode 100644
index 0000000..96cb9b0
--- /dev/null
+++ b/inc/integrations/services/CloudflareOld.php
@@ -0,0 +1,266 @@
+<?php
+
+namespace JVBase\integrations\services;
+use JVBase\integrations\Integrations;
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class CloudflareOld extends Integrations
+{
+	private string $site_key;
+	private string $secret_key;
+	private string $theme = 'light';
+	private string $size = 'normal';
+
+	protected static array $instances = [];
+	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 self($userID);
+		}
+		return self::$instances[$key];
+	}
+
+	protected function __construct()
+	{
+		$this->service_name = 'cloudflare';
+		$this->title = 'Cloudflare Turnstile';
+		$this->icon = 'cloud';
+		$this->apiBase = 'https://challenges.cloudflare.com';
+		$this->apiEndpoints = ['turnstile/v0/siteverify'];
+
+		$this->fields = [
+			'site_key'	=> [
+				'label'	=> 'Site Key',
+				'type'	=> 'text',
+				'placeholder'	=> 'Enter Public key for Turnstile widget display.',
+				'hint'	=> 'Public key for Turnstile widget',
+				'required'	=> true
+			],
+			'secret_key'	=> [
+				'label'	=> 'Secret Key',
+				'type'	=> 'text',
+				'subtype'	=> 'password',
+				'placeholder'	=> 'Enter Cloudflare Turnstile Secret Key',
+				'required'	=> true,
+				'hint'	=> 'Secret key for server-side verification'
+			]
+		];
+
+		$this->instructions = [
+			'Go to <a href="https://dash.cloudflare.com/?to=/:account/turnstile" target="_blank">Cloudflare Dashboard</a>',
+			'Create a new Turnstile widget',
+			'Add your domain to the widget settings',
+			'Copy the Site Key and Secret Key'
+		];
+
+		$this->advanced = [];
+		parent::__construct();
+	}
+
+	protected function initialize(): void
+	{
+		$this->site_key = $this->credentials['site_key'] ?? '';
+		$this->secret_key = $this->credentials['secret_key'] ?? '';
+		$this->theme = $this->credentials['theme'] ?? 'light';
+		$this->size = $this->credentials['size'] ?? 'normal';
+
+	}
+
+	/**
+	 * Check if scripts should be enqueued
+	 */
+	public function shouldEnqueueScripts(): bool
+	{
+		return $this->isSetUp() && !is_admin();
+	}
+
+	/**
+	 * Test connection by attempting a verification with an invalid token
+	 */
+	public function performConnectionTest(): bool
+	{
+		if (empty($this->site_key) || empty($this->secret_key)) {
+			return false;
+		}
+
+		// Test with a dummy token to verify API connectivity
+		$response = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
+			'body' => [
+				'secret' => $this->secret_key,
+				'response' => 'test-token-connection-check'
+			],
+			'timeout' => 10
+		]);
+
+		// We expect this to fail with success:false, but not with a connection error
+		if (is_wp_error($response)) {
+			$this->logError('Connection test failed: ', [
+				'message'  => $response->get_error_message()
+			]);
+			return false;
+		}
+
+		$result = json_decode(wp_remote_retrieve_body($response), true);
+
+		// If we get a response with success field (even if false), connection works
+		return array_key_exists('success', $result);
+	}
+
+	protected function validateCredentials(array $credentials): bool
+	{
+		if (empty($credentials['site_key'])) {
+			$this->logError('Missing Site Key', [
+				'method'	=> 'validateCredentials'
+			]);
+			return false;
+		}
+
+		if (empty($credentials['secret_key'])) {
+			$this->logError('Missing Secret Key', [
+				'method'	=> 'validateCredentials'
+			]);
+			return false;
+		}
+
+		// Validate format
+		if (!preg_match('/^[0-9a-zA-Z._-]+$/', $credentials['site_key'])) {
+
+			$this->logError('Invalid Site Key', [
+				'method'	=> 'validateCredentials'
+			]);
+			return false;
+		}
+
+		return true;
+	}
+
+	/**
+	 * Enqueue Turnstile script
+	 */
+	public function enqueueTurnstileScripts(): void
+	{
+		wp_enqueue_script(
+			'cloudflare-turnstile',
+			'https://challenges.cloudflare.com/turnstile/v0/api.js',
+			[],
+			null,
+			true
+		);
+		wp_script_add_data('cloudflare-turnstile', 'async', true);
+		wp_script_add_data('cloudflare-turnstile', 'defer', true);
+	}
+
+	/**
+	 * Render Turnstile widget
+	 * Can be called directly: jvbConnect('cloudflare')->renderTurnstile()
+	 */
+	public function renderTurnstile(array|string $options = []): void
+	{
+		$this->ensureInitialized();
+		if (!$this->isSetUp()) {
+			return;
+		}
+
+		if (!is_array($options)) {
+			$options = [];
+		}
+
+		$defaults = [
+			'theme' => $this->theme,
+			'size' => $this->size,
+			'wrapper_class' => 'cf-turnstile-wrapper',
+			'wrapper_style' => 'margin: 1em 0;'
+		];
+
+		$options = wp_parse_args($options, $defaults);
+
+		echo '<div class="' . esc_attr($options['wrapper_class']) . '" style="' . esc_attr($options['wrapper_style']) . '">';
+		echo '<div class="cf-turnstile" ';
+		echo 'data-sitekey="' . esc_attr($this->site_key) . '" ';
+		echo 'data-theme="' . esc_attr($options['theme']) . '" ';
+		echo 'data-size="' . esc_attr($options['size']) . '">';
+		echo '</div>';
+		echo '</div>';
+	}
+
+	/**
+	 * Verify Turnstile response
+	 * Can be called directly: JVB()->connect('cloudflare')->verifyTurnstile($token)
+	 */
+	public function verifyTurnstile(?string $token = null, string $remote_ip = ''): bool
+	{
+		$this->ensureInitialized();
+		if (!$this->isSetUp()) {
+			return false;
+		}
+
+		// If no token provided, try to get from POST
+		if (!$token && isset($_POST['cf-turnstile-response'])) {
+			$token = sanitize_text_field($_POST['cf-turnstile-response']);
+		}
+
+		if (empty($token)) {
+			return false;
+		}
+
+		$data = [
+			'secret' => $this->secret_key,
+			'response' => $token
+		];
+
+		if (empty($remote_ip) && isset($_SERVER['REMOTE_ADDR'])) {
+			$remote_ip = $_SERVER['REMOTE_ADDR'];
+		}
+
+		if (!empty($remote_ip)) {
+			$data['remoteip'] = $remote_ip;
+		}
+
+		$response = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
+			'body' => $data,
+			'timeout' => 10
+		]);
+
+		if (is_wp_error($response)) {
+			$this->logError('Turnstile verification failed: ' . $response->get_error_message());
+			return false;
+		}
+
+		$result = json_decode(wp_remote_retrieve_body($response), true);
+
+		if (!($result['success'] ?? false)) {
+			$error_codes = implode(', ', $result['error-codes'] ?? ['Unknown error']);
+			$this->logError('Turnstile verification failed: ' . $error_codes);
+		}
+
+		return $result['success'] ?? false;
+	}
+
+	/**
+	 * Get site key for frontend use
+	 */
+	public function getSiteKey(): string
+	{
+		return $this->site_key;
+	}
+
+	/**
+	 * Get service description
+	 */
+	public function getServiceDescription(): string
+	{
+		return "Add Turnstile captcha protection to your forms and login pages to prevent spam and bot submissions.";
+	}
+
+	/**
+	 * These methods are not used by Cloudflare but must be implemented
+	 */
+	protected function getRequestHeaders(): array
+	{
+		// Cloudflare Turnstile uses POST parameters, not headers
+		return [];
+	}
+}
diff --git a/inc/integrations/services/FacebookOld.php b/inc/integrations/services/FacebookOld.php
new file mode 100644
index 0000000..5e046ea
--- /dev/null
+++ b/inc/integrations/services/FacebookOld.php
@@ -0,0 +1,701 @@
+<?php
+/**
+ * Facebook Integration
+ * Handles Facebook posts, page information, events, and OAuth authentication
+ */
+
+namespace JVBase\integrations\services;
+use JVBase\integrations\Integrations;
+use JVBase\meta\Meta;
+use WP_Post;
+use WP_Error;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class FacebookOld extends Integrations
+{
+	// Facebook-specific properties
+	protected array $allowedContent = [
+		'post',
+		'photo',
+		'video',
+		'event',
+		'offer',
+		'note',
+		'milestone'
+	];
+	private string $page_id = '';
+	private string $page_access_token = '';
+	private array $permissions = [];
+	private array $pages = [];
+
+	// Post type mapping for Facebook
+	private const FB_POST_TYPES = [
+		'post' => 'feed',
+		'photo' => 'photos',
+		'video' => 'videos',
+		'event' => 'events',
+		'offer' => 'offers',
+		'note' => 'notes',
+		'milestone' => 'milestones'
+	];
+
+	protected static array $instances = [];
+	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 self($userID);
+		}
+		return self::$instances[$key];
+	}
+
+	protected function __construct(?int $userID = null)
+	{
+		$this->service_name = 'facebook';
+		$this->title = 'Facebook';
+		$this->icon = 'facebook-logo';
+
+		// API Configuration
+		$this->apiBase = [
+			'graph' => 'https://graph.facebook.com/v18.0',
+			'auth' => 'https://www.facebook.com/v18.0'
+		];
+
+		$this->apiVersion = 'v18.0';
+
+		$this->isOAuthService = true;
+		// OAuth Configuration
+		$this->oauth = [
+			'authorize' => 'https://www.facebook.com/v18.0/dialog/oauth',
+			'token' => 'https://graph.facebook.com/v18.0/oauth/access_token',
+			'revoke' => 'https://graph.facebook.com/v18.0/me/permissions',
+			'scopes' => [
+				'pages_show_list',
+				'pages_read_engagement',
+				'pages_manage_posts',
+				'pages_manage_metadata',
+				'pages_manage_engagement',
+				'pages_read_user_content',
+				'business_management',
+				'instagram_basic',
+				'instagram_content_publish'
+			]
+		];
+
+		// Sync capabilities
+		$this->canSync = [
+			'initial' => true,
+			'update' => true,
+			'delete' => true
+		];
+
+		// Rate limits for Facebook Graph API
+		$this->rate_limits = [
+			'per_second' => 10,
+			'per_minute' => 600,
+			'per_hour' => 200000
+		];
+
+		$this->fields = [
+			'client_id'	=> [
+				'type'	=> 'text',
+				'placeholder' => 'Enter Facebook App ID',
+				'hint'	=> 'Your Facebook App ID from developers.facebook.com',
+				'label'	=> 'App ID',
+				'required'	=> true,
+			],
+			'client_secret' => [
+				'type'	=> 'text',
+				'subtype'=> 'password',
+				'label'	=> 'App Secret',
+				'required'	=> true,
+				'hint'	=> 'Your Facebook App Secret (keep this secure!)'
+			],
+		];
+
+		$this->advanced = [
+
+		];
+
+		$this->instructions = [
+
+		];
+
+		$this->defaults = [
+
+		];
+		$this->hasWebhooks = true;
+
+		parent::__construct();
+
+		$this->actions = array_merge($this->actions, [
+			'sync_pages'	=> 'getUserPages'
+		]);
+	}
+
+	/**
+	 * Initialize Facebook-specific properties
+	 */
+	protected function initialize(): void
+	{
+		$this->page_id = $this->credentials['page_id'] ?? '';
+		$this->page_access_token = $this->credentials['page_access_token'] ?? '';
+		$this->permissions = $this->credentials['permissions'] ?? [];
+
+		// Build dynamic endpoints based on page
+		if ($this->page_id) {
+			$this->apiEndpoints = [
+				"/{$this->page_id}/feed",
+				"/{$this->page_id}/photos",
+				"/{$this->page_id}/videos",
+				"/{$this->page_id}/events",
+				"/{$this->page_id}/insights",
+				"/{$this->page_id}/conversations",
+				"/{$this->page_id}",
+				"/me/accounts"
+			];
+		}
+	}
+
+	/**
+	 * Get service description
+	 */
+	public function getServiceDescription(): string
+	{
+		return "Connect your Facebook Page to share posts, events, and engage with your audience.";
+	}
+
+	/**
+	 * Get request headers for Facebook API
+	 */
+	protected function getRequestHeaders(): array
+	{
+		$headers = [
+			'Content-Type' => 'application/json',
+			'Accept' => 'application/json'
+		];
+
+		// Use page access token if available, otherwise user token
+		if (!empty($this->page_access_token)) {
+			// Token is sent in URL params for Facebook, not headers
+		}
+
+		return $headers;
+	}
+
+	/**
+	 * Add Facebook-specific OAuth parameters
+	 */
+	protected function addOAuthParams(array $params): array
+	{
+		$params['display'] = 'popup';
+		$params['auth_type'] = 'rerequest';
+		$params['return_scopes'] = 'true';
+		return $params;
+	}
+
+	/**
+	 * Add Facebook-specific credential data after OAuth
+	 */
+	protected function addCredentialData(array $credentials, array $tokens): array
+	{
+		// Get user's pages after OAuth
+		try {
+			$pages = $this->getUserPages($tokens['access_token']);
+			if (!empty($pages)) {
+				$credentials['pages'] = $pages;
+				// Auto-select first page if not already selected
+				if (empty($credentials['page_id']) && !empty($pages[0])) {
+					$credentials['page_id'] = $pages[0]['id'];
+					$credentials['page_access_token'] = $pages[0]['access_token'];
+					$credentials['page_name'] = $pages[0]['name'];
+				}
+			}
+
+			// Store permissions granted
+			$permissions = $this->getGrantedPermissions($tokens['access_token']);
+			$credentials['permissions'] = $permissions;
+
+		} catch (Exception $e) {
+			$this->logError('Failed to fetch Facebook pages', ['error' => $e->getMessage()]);
+		}
+
+		return $credentials;
+	}
+
+	/**
+	 * Test Facebook connection
+	 */
+	protected function performConnectionTest(): bool
+	{
+		if (empty($this->credentials['access_token'])) {
+			return false;
+		}
+
+		try {
+			// Test by fetching user info
+			$response = wp_remote_get(
+				$this->apiBase['graph'] . '/me?access_token=' . $this->credentials['access_token']
+			);
+
+			if (is_wp_error($response)) {
+				return false;
+			}
+
+			$code = wp_remote_retrieve_response_code($response);
+			return $code === 200;
+
+		} catch (Exception $e) {
+			$this->logError('Connection test failed', ['error' => $e->getMessage()]);
+			return false;
+		}
+	}
+
+	/**
+	 * Render Additional OAuth settings
+	 */
+	protected function renderOAuthConnectedOptions(): void
+	{
+		// Page selection dropdown
+		if (!empty($this->credentials['pages'])) {
+			?>
+			<div class="form-field">
+				<label for="fb_page">Active Facebook Page</label>
+				<select name="credentials[page_id]" id="fb_page">
+					<option value="">Select a page...</option>
+					<?php foreach ($this->credentials['pages'] as $page): ?>
+						<option value="<?php echo esc_attr($page['id']); ?>"
+							<?php selected($this->credentials['page_id'] ?? '', $page['id']); ?>>
+							<?php echo esc_html($page['name']); ?>
+						</option>
+					<?php endforeach; ?>
+				</select>
+			</div>
+			<?php
+		}
+	}
+
+	/**
+	 * Validate webhook from Facebook
+	 */
+	protected function validateWebhook(array $payload): bool
+	{
+		// Handle webhook verification challenge
+		if (isset($payload['hub_mode']) && $payload['hub_mode'] === 'subscribe') {
+			$verify_token = $this->credentials['webhook_verify_token'] ?? '';
+			if ($payload['hub_verify_token'] === $verify_token) {
+				// Echo the challenge for verification
+				echo $payload['hub_challenge'];
+				return true;
+			}
+			return false;
+		}
+
+		// Validate webhook signature for actual events
+		$headers = $payload['_headers'] ?? [];
+		$signature = $headers['x-hub-signature-256'] ?? '';
+
+		if (empty($signature)) {
+			$this->logError('Missing webhook signature');
+			return false;
+		}
+
+		// Calculate expected signature
+		$app_secret = $this->credentials['client_secret'] ?? '';
+		$expected = 'sha256=' . hash_hmac('sha256', json_encode($payload), $app_secret);
+
+		return hash_equals($expected, $signature);
+	}
+
+	/**
+	 * Process webhook from Facebook
+	 */
+	protected function processWebhook(array $payload): bool
+	{
+		try {
+			$object = $payload['object'] ?? '';
+			$entry = $payload['entry'] ?? [];
+
+			foreach ($entry as $item) {
+				$changes = $item['changes'] ?? [];
+
+				foreach ($changes as $change) {
+					$field = $change['field'] ?? '';
+					$value = $change['value'] ?? [];
+
+					switch ($field) {
+						case 'feed':
+							$this->handleFeedUpdate($value);
+							break;
+
+						case 'conversations':
+							$this->handleMessageUpdate($value);
+							break;
+
+						case 'photos':
+							$this->handlePhotoUpdate($value);
+							break;
+
+						default:
+							$this->logDebug('Unhandled webhook field', ['field' => $field]);
+					}
+				}
+			}
+
+			return true;
+
+		} catch (Exception $e) {
+			$this->logError('Webhook processing failed', [
+				'error' => $e->getMessage(),
+				'payload' => $payload
+			]);
+			return false;
+		}
+	}
+
+	/**
+	 * Handle post save for Facebook sync
+	 */
+	protected function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings): void
+	{
+
+		if (!$this->hasOAuthCredentials()) {
+			error_log('OAuth Not set up for '.$this->service_name);
+			return;
+		}
+		$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');
+	}
+
+	/**
+	 * Process queued operations
+	 */
+	public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array
+	{
+		try {
+			$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);
+
+				case 'facebook_update_post':
+					return $fb->updateFacebookPost($data);
+
+				case 'facebook_delete_post':
+					return $fb->deleteFacebookPost($data);
+
+				case 'facebook_create_event':
+					return $fb->createFacebookEvent($data);
+
+				default:
+					return $result;
+			}
+		} catch (Exception $e) {
+			$this->logError('Operation failed', [
+				'type' => $operation->type,
+				'error' => $e->getMessage()
+			]);
+			return new WP_Error('operation_failed', $e->getMessage());
+		}
+	}
+
+	/**
+	 * Create a Facebook post
+	 */
+	private function createFacebookPost(array $data): array
+	{
+		$post = get_post($data['post_id']);
+		if (!$post) {
+			return ['success' => false, 'error' => 'Post not found'];
+		}
+
+		// Build Facebook post data
+		$fb_data = [
+			'message' => $this->formatPostContent($post),
+			'access_token' => $this->page_access_token
+		];
+
+		// Add link if present
+		$link = get_post_meta($post->ID, 'link_url', true);
+		if ($link) {
+			$fb_data['link'] = $link;
+		}
+
+		// Handle featured image
+		if (has_post_thumbnail($post->ID)) {
+			$image_url = get_the_post_thumbnail_url($post->ID, 'large');
+			$fb_data['picture'] = $image_url;
+		}
+
+		// Determine endpoint
+		$endpoint = $data['endpoint'] ?? 'feed';
+		$url = $this->apiBase['graph'] . '/' . $this->page_id . '/' . $endpoint;
+
+		// Make the API request
+		$response = wp_remote_post($url, [
+			'body' => $fb_data,
+			'timeout' => 30
+		]);
+
+		if (is_wp_error($response)) {
+			return ['success' => false, 'error' => $response->get_error_message()];
+		}
+
+		$body = json_decode(wp_remote_retrieve_body($response), true);
+
+		if (isset($body['id'])) {
+			// Store Facebook post ID
+			update_post_meta($post->ID, BASE . '_facebook_post_id', $body['id']);
+			update_post_meta($post->ID, BASE . '_facebook_sync_time', time());
+
+			return [
+				'success' => true,
+				'facebook_id' => $body['id'],
+				'message' => 'Posted to Facebook successfully'
+			];
+		}
+
+		return ['success' => false, 'error' => $body['error']['message'] ?? 'Unknown error'];
+	}
+
+	/**
+	 * Update a Facebook post
+	 */
+	private function updateFacebookPost(array $data): array
+	{
+		$post = get_post($data['post_id']);
+		$fb_post_id = get_post_meta($post->ID, BASE . '_facebook_post_id', true);
+
+		if (!$fb_post_id) {
+			// No existing post, create new one
+			return $this->createFacebookPost($data);
+		}
+
+		// Facebook doesn't allow editing most post types after creation
+		// We can only update certain fields like message
+		$fb_data = [
+			'message' => $this->formatPostContent($post),
+			'access_token' => $this->page_access_token
+		];
+
+		$url = $this->apiBase['graph'] . '/' . $fb_post_id;
+
+		$response = wp_remote_request($url, [
+			'method' => 'POST',
+			'body' => $fb_data
+		]);
+
+		if (is_wp_error($response)) {
+			return ['success' => false, 'error' => $response->get_error_message()];
+		}
+
+		update_post_meta($post->ID, BASE . '_facebook_sync_time', time());
+
+		return ['success' => true, 'message' => 'Updated on Facebook'];
+	}
+
+	/**
+	 * Delete a Facebook post
+	 */
+	private function deleteFacebookPost(array $data): array
+	{
+		$fb_post_id = get_post_meta($data['post_id'], BASE . '_facebook_post_id', true);
+
+		if (!$fb_post_id) {
+			return ['success' => true, 'message' => 'No Facebook post to delete'];
+		}
+
+		$url = $this->apiBase['graph'] . '/' . $fb_post_id . '?access_token=' . $this->page_access_token;
+
+		$response = wp_remote_request($url, ['method' => 'DELETE']);
+
+		if (!is_wp_error($response)) {
+			delete_post_meta($data['post_id'], BASE . '_facebook_post_id');
+			delete_post_meta($data['post_id'], BASE . '_facebook_sync_time');
+		}
+
+		return ['success' => true, 'message' => 'Deleted from Facebook'];
+	}
+
+	/**
+	 * Create a Facebook event
+	 */
+	private function createFacebookEvent(array $data): array
+	{
+		$post = get_post($data['post_id']);
+		$meta = Meta::forPost($post->ID);
+
+		$event_data = [
+			'name' => $post->post_title,
+			'description' => $this->formatPostContent($post),
+			'start_time' => $meta->get('event_start_date'),
+			'end_time' => $meta->get('event_end_date'),
+			'access_token' => $this->page_access_token
+		];
+
+		// Add location if available
+		$location = $meta->get('event_location');
+		if ($location) {
+			$event_data['location'] = $location;
+		}
+
+		$url = $this->apiBase['graph'] . '/' . $this->page_id . '/events';
+
+		$response = wp_remote_post($url, [
+			'body' => $event_data
+		]);
+
+		if (is_wp_error($response)) {
+			return ['success' => false, 'error' => $response->get_error_message()];
+		}
+
+		$body = json_decode(wp_remote_retrieve_body($response), true);
+
+		if (isset($body['id'])) {
+			update_post_meta($post->ID, BASE . '_facebook_event_id', $body['id']);
+			return ['success' => true, 'event_id' => $body['id']];
+		}
+
+		return ['success' => false, 'error' => $body['error']['message'] ?? 'Unknown error'];
+	}
+
+	/**
+	 * Helper: Get user's Facebook pages
+	 */
+	private function getUserPages(?string $access_token = null): array
+	{
+		$token = $access_token ?: $this->credentials['access_token'];
+
+		$url = $this->apiBase['graph'] . '/me/accounts?access_token=' . $token;
+
+		$response = wp_remote_get($url);
+
+		if (is_wp_error($response)) {
+			throw new Exception($response->get_error_message());
+		}
+
+		$body = json_decode(wp_remote_retrieve_body($response), true);
+
+		if (isset($body['error'])) {
+			throw new Exception($body['error']['message']);
+		}
+
+		return $body['data'] ?? [];
+	}
+
+	/**
+	 * Helper: Get granted permissions
+	 */
+	private function getGrantedPermissions(string $access_token): array
+	{
+		$url = $this->apiBase['graph'] . '/me/permissions?access_token=' . $access_token;
+
+		$response = wp_remote_get($url);
+
+		if (is_wp_error($response)) {
+			return [];
+		}
+
+		$body = json_decode(wp_remote_retrieve_body($response), true);
+		$permissions = [];
+
+		foreach (($body['data'] ?? []) as $permission) {
+			if ($permission['status'] === 'granted') {
+				$permissions[] = $permission['permission'];
+			}
+		}
+
+		return $permissions;
+	}
+
+	/**
+	 * Helper: Format post content for Facebook
+	 */
+	private function formatPostContent(WP_Post $post): string
+	{
+		$content = $post->post_content;
+
+		// Strip shortcodes
+		$content = strip_shortcodes($content);
+
+		// Convert to plain text
+		$content = wp_strip_all_tags($content);
+
+		// Add link to full post
+		$permalink = get_permalink($post->ID);
+		$content .= "\n\nRead more: " . $permalink;
+
+		// Trim to Facebook's character limit (63,206 characters)
+		if (strlen($content) > 63000) {
+			$content = substr($content, 0, 63000) . '...';
+		}
+
+		return $content;
+	}
+
+	/**
+	 * Helper: Handle feed updates from webhook
+	 */
+	private function handleFeedUpdate(array $data): void
+	{
+		// Process incoming feed updates
+		// Could sync comments, reactions, etc. back to WordPress
+		$this->logDebug('Feed update received', $data);
+	}
+
+	/**
+	 * Helper: Handle message updates from webhook
+	 */
+	private function handleMessageUpdate(array $data): void
+	{
+		// Process incoming messages
+		// Could create notifications or queue responses
+		$this->logDebug('Message received', $data);
+	}
+
+	/**
+	 * Helper: Handle photo updates from webhook
+	 */
+	private function handlePhotoUpdate(array $data): void
+	{
+		// Process photo updates
+		$this->logDebug('Photo update received', $data);
+	}
+
+	/**
+	 * Send test post to Facebook
+	 */
+	private function sendTestPost(): array
+	{
+		$test_data = [
+			'message' => 'Test post from ' . get_bloginfo('name') . ' at ' . current_time('mysql'),
+			'access_token' => $this->page_access_token
+		];
+
+		$url = $this->apiBase['graph'] . '/' . $this->page_id . '/feed';
+
+		$response = wp_remote_post($url, [
+			'body' => $test_data
+		]);
+
+		if (is_wp_error($response)) {
+			return ['success' => false, 'error' => $response->get_error_message()];
+		}
+
+		$body = json_decode(wp_remote_retrieve_body($response), true);
+
+		if (isset($body['id'])) {
+			return ['success' => true, 'post_id' => $body['id']];
+		}
+
+		return ['success' => false, 'error' => $body['error']['message'] ?? 'Unknown error'];
+	}
+}
diff --git a/inc/integrations/services/GoogleMapsOld.php b/inc/integrations/services/GoogleMapsOld.php
new file mode 100644
index 0000000..aebaf87
--- /dev/null
+++ b/inc/integrations/services/GoogleMapsOld.php
@@ -0,0 +1,834 @@
+<?php
+/**
+ * Google Maps Integration
+ * File: /inc/integrations/GoogleMapsIntegration.php
+ */
+
+namespace JVBase\integrations\services;
+use JVBase\integrations\Integrations;
+use JVBase\registrar\Registrar;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class GoogleMapsOld extends Integrations
+{
+	private string $public_key;
+	private string $private_key;
+	private string $map_id;
+	private array $enabled_apis = [];
+
+	// Edmonton default coordinates
+	private const DEFAULT_LAT = 53.5461;
+	private const DEFAULT_LNG = -113.4938;
+	private const DEFAULT_ZOOM = 11; // City-wide view
+	private const DEFAULT_MAP_ID = '873aed4ca260b4203e45b6e3';
+
+	protected static self $instance;
+	public static function getInstance():self
+	{
+
+		if (!isset(self::$instance)) {
+			self::$instance = new self();
+		}
+		return self::$instance;
+	}
+
+	protected function __construct()
+	{
+		$this->service_name = 'maps';
+		$this->title = 'Google Maps';
+		$this->icon = 'map-pin';
+		$this->apiBase = 'https://maps.googleapis.com/maps/api/';
+		$this->apiEndpoints = [
+			'geocode/json',
+			'place/details/json',
+			'place/nearbysearch/json',
+			'distancematrix/json',
+			'js'
+		];
+
+		$this->fields = [
+			'public_key' => [
+				'type'	=> 'text',
+				'subtype' => 'password',
+				'label'	=> 'Maps Platform API Key (Referrer-Restricted)',
+				'hint'	=> 'Restrict this key by <b>HTTP referrers</b> for security. Used for displaying maps in browsers.',
+				'required'	=> true,
+			],
+			'private_key' => [
+				'type'	=> 'text',
+				'subtype'	=> 'password',
+				'label'	=> 'Geocoding & Places API Key (IP-Restricted)',
+				'hint'	=> 'Restrict this key by <br>IP Addresses</b>. Used for geocoding and server-side operations.',
+				'required'	=> true,
+			]
+		];
+		$this->advanced = [
+			'map_id'	=> [
+				'type'	=> 'text',
+				'label'	=> 'Map ID',
+				'hint'	=> 'Required for custom styling. Create a Map ID in <a href="https://console.cloud.google.com/google/maps-apis/studio/maps" target="_blank">Google Cloud Console → Maps → Map Management</a>'
+			],
+			'default_zoom'	=> [
+				'type'	=> 'text',
+				'subtype'	=> 'number',
+				'min'	=> 1,
+				'max'	=> 20,
+				'label'	=> 'Default Zoom',
+				'default'	=> 11
+			],
+		];
+		$this->instructions = [
+			'Create two API keys in <a href="https://console.cloud.google.com/apis/credentials" target="_blank">Google Cloud Console</a>',
+			'Enable <strong>Geocoding API + Places API</strong> for frontend key, restricted by your server IP address',
+			'Enable <strong>Maps Platform API</strong> for server key, restricted by your website domain',
+		];
+		$this->defaults = [
+
+		];
+		parent::__construct();
+
+		$this->actions = array_merge(
+			$this->actions,
+			[
+				'reverse_geocode' => 'processReverseGeocode',
+				'geocode' => 'processGeocode'
+			]
+		);
+		// Auto-enqueue maps script when needed
+		add_action('wp_enqueue_scripts', [$this, 'maybeEnqueueMapsScript']);
+//		add_action('admin_enqueue_scripts', [$this, 'maybeEnqueueMapsScript']);
+	}
+
+	protected function initialize(): void
+	{
+		$this->private_key = $this->credentials['private_key'] ?? '';
+		$this->public_key = $this->credentials['public_key'] ?? '';
+		$this->map_id = $this->credentials['map_id'] ?? self::DEFAULT_MAP_ID;
+		$this->rate_limits = ['min_interval' => 0]; // Google Maps has generous limits
+	}
+
+	/**
+	 * Get service description
+	 */
+	public function getServiceDescription(): string
+	{
+		return "Enable location features and embedded maps throughout your site.";
+	}
+
+	/**
+	 * Get request headers (required by base class)
+	 */
+	protected function getRequestHeaders(): array
+	{
+		// Google Maps API uses API key in URL params, not headers
+		return [
+			'Accept' => 'application/json',
+			'Content-Type' => 'application/json'
+		];
+	}
+
+
+	/**
+	 * Process test connection request
+	 */
+	protected function performConnectionTest(): bool
+	{
+		if (!$this->isSetUp()) {
+			return false;
+		}
+
+		// Test with a simple geocoding request
+		$test_address = 'Edmonton, Alberta, Canada';
+		$result = $this->geocodeAddress($test_address);
+
+		return $result !== null;
+	}
+
+	/**
+	 * Process reverse geocode request
+	 */
+	private function processReverseGeocode(array $data): WP_Error|array
+	{
+		$lat = floatval($data['lat'] ?? 0);
+		$lng = floatval($data['lng'] ?? 0);
+
+		if (!$lat || !$lng) {
+			return new WP_Error('invalid_coordinates', 'Invalid coordinates provided');
+		}
+
+		$result = $this->reverseGeocode($lat, $lng);
+
+		if ($result) {
+			return ['success' => true, 'data' => $result];
+		}
+
+		return new WP_Error('geocoding_failed', 'Reverse geocoding failed');
+	}
+
+	/**
+	 * Process geocode request
+	 */
+	private function processGeocode(array $data): WP_Error|array
+	{
+		$address = sanitize_text_field($data['address'] ?? '');
+
+		if (empty($address)) {
+			return new WP_Error('invalid_address', 'No address provided');
+		}
+
+		$result = $this->geocodeAddress($address);
+
+		if ($result) {
+			return ['success' => true, 'data' => $result];
+		}
+
+		return new WP_Error('geocoding_failed', 'Geocoding failed');
+	}
+
+	/**
+	 * Geocode an address to coordinates
+	 */
+	public function geocodeAddress(string $address): ?array
+	{
+		if (!$this->isSetUp()) {
+			return null;
+		}
+
+		$params = [
+			'address' => $address,
+			'key' => $this->private_key
+		];
+
+		$key = $this->cache->generateKey(['geocode', $address]);
+		$cached = $this->cache->get($key);
+		if ($cached) {
+			return $cached;
+		}
+
+		$url = $this->apiBase . 'geocode/json?' . http_build_query($params);
+		$response = wp_remote_get($url);
+
+		if (is_wp_error($response)) {
+			$this->logError('Geocoding request failed', [
+				'error' => $response->get_error_message(),
+				'address' => $address
+			]);
+			return null;
+		}
+
+		$data = json_decode(wp_remote_retrieve_body($response), true);
+
+		if ($data['status'] === 'OK' && !empty($data['results'])) {
+			$result = $data['results'][0];
+			$result = [
+				'lat' => $result['geometry']['location']['lat'],
+				'lng' => $result['geometry']['location']['lng'],
+				'formatted_address' => $result['formatted_address'],
+				'place_id' => $result['place_id'] ?? '',
+				'components' => $this->parseAddressComponents($result['address_components'] ?? [])
+			];
+			$this->cache->set($key, $result);
+		} else {
+			$this->logError('Google Maps API error', [
+				'status' => $data['status'] ?? 'unknown',
+				'address' => $address
+			], 'warning');
+			$result = null;
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Reverse geocode coordinates to address
+	 */
+	public function reverseGeocode(float $lat, float $lng): ?array
+	{
+		if (!$this->isSetUp()) {
+			return null;
+		}
+
+		$params = [
+			'latlng' => "{$lat},{$lng}",
+			'key' => $this->private_key
+		];
+
+		$key = $this->cache->generateKey(['reverse', $lat, $lng]);
+		$cached = $this->cache->get($key);
+		if ($cached) {
+			return $cached;
+		}
+
+		$url = $this->apiBase . 'geocode/json?' . http_build_query($params);
+		$response = wp_remote_get($url);
+
+		if (is_wp_error($response)) {
+			$this->logError('Reverse geocoding request failed', [
+				'error' => $response->get_error_message(),
+				'lat' => $lat,
+				'lng' => $lng
+			]);
+			return null;
+		}
+
+		$data = json_decode(wp_remote_retrieve_body($response), true);
+
+		if ($data['status'] === 'OK' && !empty($data['results'])) {
+			$result = $data['results'][0];
+			$return = [
+				'formatted_address' => $result['formatted_address'],
+				'place_id' => $result['place_id'] ?? '',
+				'components' => $this->parseAddressComponents($result['address_components'] ?? [])
+			];
+			$this->cache->set($key, $return);
+			return $return;
+		}
+
+		return null;
+	}
+
+	/**
+	 * Get place details by place ID
+	 */
+	public function getPlaceDetails(string $place_id, array $fields = []): ?array
+	{
+		if (!$this->isSetUp()) {
+			return null;
+		}
+
+		$default_fields = ['name', 'formatted_address', 'geometry', 'rating', 'formatted_phone_number'];
+		$fields = empty($fields) ? $default_fields : $fields;
+
+		$key = $this->cache->generateKey(['place', $place_id, $fields]);
+		$cached = $this->cache->get($key);
+		if ($cached) {
+			return $cached;
+		}
+
+		$params = [
+			'place_id' => $place_id,
+			'fields' => implode(',', $fields),
+			'key' => $this->private_key
+		];
+
+		$url = $this->apiBase . 'place/details/json?' . http_build_query($params);
+		$response = wp_remote_get($url);
+
+		if (is_wp_error($response)) {
+			$this->logError('Place details request failed', [
+				'error' => $response->get_error_message(),
+				'place_id' => $place_id
+			]);
+			return null;
+		}
+
+		$data = json_decode(wp_remote_retrieve_body($response), true);
+
+		if ($data['status'] === 'OK' && !empty($data['result'])) {
+			$result = $data['result'];
+			$this->cache->set($key, $result);
+			return $result;
+		}
+
+		return null;
+	}
+
+	/**
+	 * Search for places nearby
+	 */
+	public function searchNearby(float $lat, float $lng, int $radius = 1000, string $type = ''): array
+	{
+		if (!$this->isSetUp()) {
+			return [];
+		}
+
+		$params = [
+			'location' => "{$lat},{$lng}",
+			'radius' => $radius,
+			'key' => $this->private_key
+		];
+
+		if (!empty($type)) {
+			$params['type'] = $type;
+		}
+
+		$key = $this->cache->generateKey(['nearby', $lat, $lng, $radius, $type]);
+		$cached = $this->cache->get($key);
+		if ($cached) {
+			return $cached;
+		}
+
+		$url = $this->apiBase . 'place/nearbysearch/json?' . http_build_query($params);
+		$response = wp_remote_get($url);
+
+		if (is_wp_error($response)) {
+			$this->logError('Nearby search request failed', [
+				'error' => $response->get_error_message(),
+				'lat' => $lat,
+				'lng' => $lng,
+				'radius' => $radius
+			]);
+			return [];
+		}
+
+		$data = json_decode(wp_remote_retrieve_body($response), true);
+
+		if ($data['status'] === 'OK' && !empty($data['results'])) {
+			$return = $data['results'];
+			$this->cache->set($key, $return);
+			return $return;
+		}
+
+		return [];
+	}
+
+	/**
+	 * Calculate distance between two points
+	 */
+	public function calculateDistance(array $origin, array $destination, string $mode = 'driving'): ?array
+	{
+		if (!$this->isSetUp()) {
+			return null;
+		}
+
+		// Handle different input formats
+		$origin_str = is_array($origin) ? "{$origin['lat']},{$origin['lng']}" : $origin;
+		$dest_str = is_array($destination) ? "{$destination['lat']},{$destination['lng']}" : $destination;
+
+		$params = [
+			'origins' => $origin_str,
+			'destinations' => $dest_str,
+			'mode' => $mode,
+			'units' => 'metric',
+			'key' => $this->private_key
+		];
+
+		$key = $this->cache->generateKey(['distance', $origin_str, $dest_str, $mode]);
+		$cached = $this->cache->get($key);
+		if ($cached) {
+			return $cached;
+		}
+
+		$url = $this->apiBase . 'distancematrix/json?' . http_build_query($params);
+		$response = wp_remote_get($url);
+
+		if (is_wp_error($response)) {
+			$this->logError('Distance calculation request failed', [
+				'error' => $response->get_error_message(),
+				'origin' => $origin_str,
+				'destination' => $dest_str
+			]);
+			return null;
+		}
+
+		$data = json_decode(wp_remote_retrieve_body($response), true);
+
+		if ($data['status'] === 'OK' && !empty($data['rows'][0]['elements'][0])) {
+			$element = $data['rows'][0]['elements'][0];
+			if ($element['status'] === 'OK') {
+				$return = [
+					'distance' => $element['distance'],
+					'duration' => $element['duration'],
+					'mode' => $mode
+				];
+				$this->cache->set($key, $return);
+				return $return;
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Get the JavaScript API URL with key
+	 */
+	public function getJavaScriptApiUrl(array $libraries = ['places', 'marker', 'geometry']): string
+	{
+		if (empty($this->public_key)) {
+			return '';
+		}
+
+		$params = [
+			'key' => $this->public_key,
+			'libraries' => implode(',', $libraries),
+			'callback' => 'initGoogleMaps',
+		];
+
+		return $this->apiBase . 'js?' . http_build_query($params);
+	}
+
+	/**
+	 * Enqueue Google Maps script when needed
+	 */
+	public function maybeEnqueueMapsScript(): void
+	{
+		// Only enqueue if we're on a page that needs maps
+		if (!$this->shouldEnqueueMaps()) {
+			return;
+		}
+
+		if (!$this->isSetUp()) {
+			return;
+		}
+
+		$this->ensureInitialized();
+
+		// Register Google Maps API
+		wp_register_script(
+			'google-maps-api',
+			$this->getJavaScriptApiUrl(['places', 'geometry', 'marker']),
+			[],
+			null,
+			[
+				'in_footer' => true,
+				'strategy' => 'async'
+			]
+		);
+
+		// Register our maps handler
+		wp_register_script(
+			'jvb-maps',
+			JVB_URL . 'assets/js/min/maps.min.js',
+			['google-maps-api'],
+			'1.0.0',
+			[
+				'in_footer' => true,
+				'strategy' => 'defer'
+			]
+		);
+
+		// Add initialization script
+		wp_add_inline_script('google-maps-api', '
+		// Set map defaults
+		window.jvbMapDefaults = {
+			lat: ' . self::DEFAULT_LAT . ',
+			lng: ' . self::DEFAULT_LNG . ',
+			zoom: ' . self::DEFAULT_ZOOM . ',
+			mapId: "' . esc_js($this->map_id) . '"
+		};
+
+		// Google Maps initialization callback
+		window.initGoogleMaps = function() {
+			// Dispatch ready event
+			document.dispatchEvent(new Event("googleMapsReady"));
+
+			// Call any registered callbacks
+			if (window.googleMapsCallbacks && Array.isArray(window.googleMapsCallbacks)) {
+				window.googleMapsCallbacks.forEach(function(callback) {
+					if (typeof callback === "function") {
+						callback();
+					}
+				});
+				window.googleMapsCallbacks = [];
+			}
+		};
+
+		// Initialize callback queue
+		window.googleMapsCallbacks = window.googleMapsCallbacks || [];
+		', 'before');
+
+		// Enqueue scripts
+		wp_enqueue_script('google-maps-api');
+		wp_enqueue_script('jvb-maps');
+
+		// Add AJAX URL and nonce for JavaScript AJAX calls
+		wp_localize_script('jvb-maps', 'jvbMapsAjax', [
+			'ajaxurl' => admin_url('admin-ajax.php'),
+			'nonce' => wp_create_nonce('jvb_integration_' . $this->service_name),
+			'service' => $this->service_name
+		]);
+	}
+
+	/**
+	 * Check if maps should be enqueued on current page
+	 */
+	private function shouldEnqueueMaps(): bool
+	{
+		// Check for shortcodes
+		if (is_singular() && get_post_meta(get_the_ID(), BASE . 'has_map', true) === true) {
+			return true;
+		}
+
+		if (!empty(Registrar::withFeature('is_content', 'term')) && get_term_meta(get_queried_object_id(), BASE . 'has_map', true) === true) {
+			return true;
+		}
+
+		// Check for specific page templates or post types
+		if (is_singular(['shop', 'artist', 'location'])) {
+			return true;
+		}
+
+		// Check for admin pages that need maps
+		if (is_admin()) {
+			$screen = get_current_screen();
+			if ($screen && in_array($screen->post_type, ['shop', 'artist', 'location'])) {
+				return true;
+			}
+		}
+
+		// Allow filtering
+		return apply_filters('jvb_should_enqueue_maps', false);
+	}
+
+	/**
+	 * Parse address components from Google response
+	 */
+	private function parseAddressComponents(array $components): array
+	{
+		$parsed = [
+			'street_number' => '',
+			'street' => '',
+			'city' => '',
+			'province' => '',
+			'country' => '',
+			'postal_code' => ''
+		];
+
+		foreach ($components as $component) {
+			$types = $component['types'];
+
+			if (in_array('street_number', $types)) {
+				$parsed['street_number'] = $component['long_name'];
+			}
+			if (in_array('route', $types)) {
+				$parsed['street'] = $component['long_name'];
+			}
+			if (in_array('locality', $types)) {
+				$parsed['city'] = $component['long_name'];
+			}
+			if (in_array('administrative_area_level_1', $types)) {
+				$parsed['province'] = $component['short_name'];
+			}
+			if (in_array('country', $types)) {
+				$parsed['country'] = $component['short_name'];
+			}
+			if (in_array('postal_code', $types)) {
+				$parsed['postal_code'] = $component['long_name'];
+			}
+		}
+
+		// Combine street number and street name
+		if ($parsed['street_number'] && $parsed['street']) {
+			$parsed['street'] = $parsed['street_number'] . ' ' . $parsed['street'];
+		}
+
+		return $parsed;
+	}
+
+	/**
+	 * Get Map ID
+	 */
+	public function getMapId(): string
+	{
+		return $this->map_id;
+	}
+
+	/**
+	 * Get default map settings
+	 */
+	public function getDefaultMapSettings(): array
+	{
+		return [
+			'center' => [
+				'lat' => self::DEFAULT_LAT,
+				'lng' => self::DEFAULT_LNG
+			],
+			'zoom' => $this->credentials['default_zoom'] ?? self::DEFAULT_ZOOM,
+			'mapId' => $this->map_id,
+			'styles' => $this->getMapStyles()
+		];
+	}
+
+	/**
+	 * Get map styles based on theme
+	 */
+	private function getMapStyles(): array
+	{
+		$theme = $this->credentials['theme'] ?? 'default';
+
+		switch ($theme) {
+			case 'dark':
+				return [
+					['elementType' => 'geometry', 'stylers' => [['color' => '#242f3e']]],
+					['elementType' => 'labels.text.fill', 'stylers' => [['color' => '#746855']]],
+					['elementType' => 'labels.text.stroke', 'stylers' => [['color' => '#242f3e']]],
+					['featureType' => 'road', 'elementType' => 'geometry', 'stylers' => [['color' => '#38414e']]],
+					['featureType' => 'water', 'elementType' => 'geometry', 'stylers' => [['color' => '#17263c']]]
+				];
+
+			case 'minimal':
+				return [
+					['featureType' => 'poi', 'stylers' => [['visibility' => 'off']]],
+					['featureType' => 'transit', 'stylers' => [['visibility' => 'off']]]
+				];
+
+			default:
+				return [];
+		}
+	}
+
+	/**
+	 * Generate map application links
+	 */
+	public function generateMapLinks(float $lat, float $lng, ?string $address = null): array
+	{
+		$query = $address ? urlencode($address) : "{$lat},{$lng}";
+
+		return [
+			'google' => "https://www.google.com/maps/search/?api=1&query={$query}",
+			'apple' => "https://maps.apple.com/?ll={$lat},{$lng}",
+			'waze' => "https://waze.com/ul?ll={$lat},{$lng}&navigate=yes"
+		];
+	}
+
+	/**
+	 * Render a frontend display map
+	 */
+	public function renderDisplayMap(array $location, array $options = []): string
+	{
+		if (!$this->isSetUp() || empty($location['lat']) || empty($location['lng'])) {
+			return '';
+		}
+
+		$map_id = 'map-' . uniqid();
+		$lat = (float)$location['lat'];
+		$lng = (float)$location['lng'];
+		$address = $location['address'] ?? '';
+
+		$map_options = array_merge([
+			'height' => '250px',
+			'zoom' => 15,
+			'show_marker' => true,
+			'show_info_window' => true,
+			'info_content' => $address,
+			'interactive' => true
+		], $options);
+
+		ob_start();
+		?>
+		<div id="<?= esc_attr($map_id); ?>"
+			 class="jvb-location-map-display"
+			 style="height: <?= esc_attr($map_options['height']); ?>; border-radius: 8px; overflow: hidden;">
+		</div>
+
+		<script>
+			document.addEventListener('DOMContentLoaded', function() {
+				function initDisplayMap() {
+					if (!window.jvbMaps || !window.jvbMaps.ready) {
+						setTimeout(initDisplayMap, 100);
+						return;
+					}
+
+					window.jvbMaps.createDisplayMap('<?= esc_js($map_id); ?>', {
+						lat: <?= $lat; ?>,
+						lng: <?= $lng; ?>,
+						zoom: <?= intval($map_options['zoom']); ?>,
+						show_marker: <?= $map_options['show_marker'] ? 'true' : 'false'; ?>,
+						show_info_window: <?= $map_options['show_info_window'] ? 'true' : 'false'; ?>,
+						info_content: <?= json_encode($map_options['info_content']); ?>,
+						interactive: <?= $map_options['interactive'] ? 'true' : 'false'; ?>
+					});
+				}
+
+				if (typeof google !== 'undefined' && google.maps) {
+					initDisplayMap();
+				} else {
+					document.addEventListener('googleMapsReady', initDisplayMap);
+				}
+			});
+		</script>
+		<?php
+
+		return ob_get_clean();
+	}
+
+	/**
+	 * Render map application links
+	 */
+	public function renderMapLinks(float $lat, float $lng, ?string $address = null, array $options = []): string
+	{
+		$links = $this->generateMapLinks($lat, $lng, $address);
+
+		$link_options = array_merge([
+			'show_icons' => true,
+			'style' => 'buttons', // 'buttons' or 'text'
+			'include' => ['google', 'apple'], // which links to show
+			'target' => '_blank'
+		], $options);
+
+		$output = '<div class="jvb-map-links' . ($link_options['style'] === 'buttons' ? ' button-style' : ' text-style') . '">';
+
+		foreach ($link_options['include'] as $provider) {
+			if (!isset($links[$provider])) continue;
+
+			$icon = '';
+			$label = '';
+
+			switch ($provider) {
+				case 'google':
+					$icon = $link_options['show_icons'] ? jvbIcon('google') : '';
+					$label = 'Google Maps';
+					break;
+				case 'apple':
+					$icon = $link_options['show_icons'] ? jvbIcon('apple') : '';
+					$label = 'Apple Maps';
+					break;
+				case 'waze':
+					$icon = $link_options['show_icons'] ? jvbIcon('waze') : '';
+					$label = 'Waze';
+					break;
+			}
+
+			$output .= sprintf(
+				'<a href="%s" target="%s" class="map-link %s-maps-link">%s<span>%s</span></a>',
+				esc_url($links[$provider]),
+				esc_attr($link_options['target']),
+				esc_attr($provider),
+				$icon,
+				esc_html($label)
+			);
+		}
+
+		$output .= '</div>';
+
+		return $output;
+	}
+
+	/**
+	 * Format address for display
+	 */
+	public function formatAddress(array $location, string $format = 'full'): string
+	{
+		if (!empty($location['address'])) {
+			return $location['address'];
+		}
+
+		// Build address from components if no formatted address
+		$parts = [];
+
+		switch ($format) {
+			case 'full':
+				if (!empty($location['street'])) $parts[] = $location['street'];
+				if (!empty($location['city'])) $parts[] = $location['city'];
+				if (!empty($location['province'])) $parts[] = $location['province'];
+				if (!empty($location['postal_code'])) $parts[] = $location['postal_code'];
+				if (!empty($location['country'])) $parts[] = $location['country'];
+				break;
+
+			case 'compact':
+				if (!empty($location['city']) && !empty($location['province'])) {
+					$parts[] = $location['city'] . ', ' . $location['province'];
+				}
+				break;
+
+			case 'city_only':
+				if (!empty($location['city'])) $parts[] = $location['city'];
+				break;
+		}
+
+		return implode(', ', $parts);
+	}
+}
diff --git a/inc/integrations/services/GoogleMyBusiness.php b/inc/integrations/services/GoogleMyBusiness.php
index cd80f51..157181e 100644
--- a/inc/integrations/services/GoogleMyBusiness.php
+++ b/inc/integrations/services/GoogleMyBusiness.php
@@ -2767,7 +2767,6 @@
 
 	protected function setContentTypes():void
 	{
-		$this->has_content = true;
 		$this->contentTypes = [
 			'menu_item'	=> [
 				'nutritionFacts'	=> [
diff --git a/inc/integrations/services/GoogleMyBusinessOld.php b/inc/integrations/services/GoogleMyBusinessOld.php
new file mode 100644
index 0000000..348700f
--- /dev/null
+++ b/inc/integrations/services/GoogleMyBusinessOld.php
@@ -0,0 +1,2974 @@
+<?php
+namespace JVBase\integrations\services;
+use JVBase\integrations\Integrations;
+use JVBase\meta\Meta;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class GoogleMyBusinessOld extends Integrations
+{
+	protected array $allowedContent = [
+		'menu_item',
+		'post',
+		'event',
+	];
+	private ?string $access_token = null;
+	protected string $readMask = 'name,title,storefrontAddress,metadata,openInfo,storeCode,categories,phoneNumbers,labels,specialHours';
+	private ?string $location = null;
+	private ?string $refresh_token = null;
+	private ?string $client_id = null;
+	private ?string $client_secret = null;
+	private ?string $account_id = null;
+
+	protected static array $instances = [];
+	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 self($userID);
+		}
+		return self::$instances[$key];
+	}
+
+	protected function __construct(?int $userID = null)
+	{
+		$this->service_name = 'gmb';
+		$this->title = 'Google My Business';
+		$this->icon = 'storefront';
+		$this->cacheName = ($userID)? 'gmb_'.$userID : 'gmb';
+		$this->canSync = [
+			'initial'	=> true,
+			'update'	=> true,
+			'delete'	=> true,
+		];
+		$this->apiBase = [
+			'base' => 'https://mybusinessbusinessinformation.googleapis.com',
+			'accounts' => 'https://mybusinessaccountmanagement.googleapis.com',
+			'posts' => 'https://mybusiness.googleapis.com',
+			'performance'=> 'https://businessprofileperformance.googleapis.com',
+			'v4' => 'https://mybusiness.googleapis.com/v4',
+		];
+
+		$this->apiEndpoints = [
+			'/accounts/[^/]+/locations/[^/]+/reviews',
+			'/accounts/[^/]+/locations/[^/]+/foodMenus',
+			'/v4/accounts/[^/]+/locations/[^/]+/media',
+			'/v4/accounts/[^/]+/locations/[^/]+/localPosts',
+			'/v4/accounts/[^/]+/locations/[^/]+/localPosts/[^/]+',
+			'/accounts/[^/]+/locations/[^/]+',
+			'/v1/accounts/[^/]+/locations', // Matches /v1/accounts/{accountId}/locations
+			'/v1/accounts',
+			// Add pattern matching for dynamic location endpoints
+			'/v1/locations/[^/]+',  // Matches /v1/locations/{locationId}
+		];
+
+		$this->isOAuthService = true;
+		$this->oauth = [
+			'authorize' => 'https://accounts.google.com/o/oauth2/v2/auth',
+			'token' => 'https://oauth2.googleapis.com/token',
+			'revoke' => 'https://oauth2.googleapis.com/revoke',
+			'scopes' => [
+				'https://www.googleapis.com/auth/business.manage',
+				'https://www.googleapis.com/auth/userinfo.email',
+				'https://www.googleapis.com/auth/userinfo.profile',
+				'https://www.googleapis.com/auth/plus.business.manage'
+			]
+		];
+
+		$this->supportsWebp = false;
+
+		$this->rate_limits = [
+			'per_second' 	=> 15,       // Allow 15 requests in burst window
+			'per_minute' 	=> 60,      // 1-minute burst window
+			'per_hour' 		=> 500      // 500 requests per hour (well below Google's limits)
+		];
+
+		$this->fields = [
+			'client_id'	=> [
+				'type'	=> 'text',
+				'label'	=> 'OAuth Client ID',
+				'placeholder'=> 'Enter Google OAuth Client ID',
+				'hint'	=> 'From your Google Cloud Console OAuth credentials.',
+				'required'	=> true,
+			],
+			'client_secret'	=> [
+				'type'	=> 'text',
+				'subtype' => 'password',
+				'label'	=> 'OAuth Client Secret',
+				'required'	=> true,
+			],
+//			'access_token'	=> [
+//				'type'	=> 'text',
+//				'subtype'	=> 'password',
+//				'label'	=> 'Access Token',
+//				'hint'	=> 'Generated automagically after OAuth authorization.'
+//			],
+//			'refresh_token'	=> [
+//				'type'	=> 'text',
+//				'subtype'	=> 'password',
+//				'label'	=> 'Refresh Token',
+//				'hint'	=> 'Generated automagically after OAuth authorization.'
+//			]
+		];
+
+		$this->advanced = [
+
+		];
+
+		$this->instructions = [
+			'Go to <a href="https://console.cloud.google.com/apis/credentials" target="_blank">Google Cloud Console</a>',
+			'Create OAuth 2.0 Client ID credentials',
+			'Enable Google My Business API',
+			'Add redirect URI: <code>'.esc_html($this->getRedirectUri()).'</code>',
+			'Use the "Authorize" button below to complete OAuth flow'
+		];
+
+		parent::__construct();
+
+		$this->actions = array_merge(
+			$this->actions,
+			[
+				'update_location'	=> 'handleUpdateLocation',
+				'select_account'	=> 'handleSelectAccount',
+				'select_location'	=> 'handleSelectLocation',
+				'check_oauth_status'=> 'checkOAuthStatus'
+			]
+		);
+		$this->buttons = array_merge(
+			$this->buttons,
+			[
+				'check_oauth_status' => 'Check OAuth Status'
+			]
+		);
+
+		if (JVB_TESTING) {
+			$this->cache->flush();
+		}
+	}
+
+	protected function initialize(): void
+	{
+		if (empty($this->credentials)) {
+			$this->loadCredentials();
+		}
+		$this->access_token = (array_key_exists('access_token', $this->credentials)) ? $this->credentials['access_token'] : null;
+		$this->refresh_token = (array_key_exists('refresh_token', $this->credentials)) ? $this->credentials['refresh_token'] : null;
+		$this->client_id = (array_key_exists('client_id', $this->credentials)) ? $this->credentials['client_id'] : null;
+		$this->client_secret = (array_key_exists('client_secret', $this->credentials)) ? $this->credentials['client_secret'] : null;
+		$this->location = (array_key_exists('location', $this->credentials)) ? $this->credentials['location'] : null;
+		$this->account_id = (array_key_exists('account', $this->credentials)) ? $this->credentials['account'] : null;
+
+		if ($this->account_id) {
+			$this->apiEndpoints[] = "/v1/{$this->account_id}/locations";
+		}
+		if ($this->location) {
+			$locationName = $this->getSelectedLocationResourceName();
+			$this->apiEndpoints[] = "/v4/{$locationName}/localPosts";
+		}
+	}
+
+	protected function performConnectionTest(): bool
+	{
+		try {
+			$accounts = $this->getAccounts(true);
+			return !empty($accounts);
+		} catch (\Exception $e) {
+			return false;
+		}
+	}
+
+	/**
+	 * Check if response contains an error - Google-specific
+	 */
+	protected function isErrorResponse(array $response): bool
+	{
+		// Google APIs return errors in this format:
+		// {"error": {"code": 401, "message": "...", "status": "UNAUTHENTICATED"}}
+		return isset($response['error']) && isset($response['error']['code']);
+	}
+
+	protected function getRequestHeaders(): array
+	{
+		return [
+			'Authorization' => 'Bearer ' . $this->access_token,
+			'Content-Type' => 'application/json',
+			'Accept' => 'application/json'
+		];
+	}
+
+	protected function addOAuthParams(array $params): array
+	{
+		$params['access_type'] = 'offline';
+		$params['prompt'] = 'consent';
+		return $params;
+	}
+
+	protected function addCredentialData(array $credentials, array $tokens): array
+	{
+		// Preserve GMB-specific settings
+		$existing = $this->credentials;
+		$credentials['location'] = $existing['location'] ?? null;
+		$credentials['account'] = $existing['account'] ?? null;
+		return $credentials;
+	}
+
+	public function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings):void
+	{
+
+		if (!$this->hasOAuthCredentials()) {
+			error_log('OAuth Not set up for '.$this->service_name);
+			return;
+		}
+
+		$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) && $data['user'] !== 0) ? $this->getInstance((int)$data['user']) : $this;
+			$gmb->ensureInitialized();
+			switch ($operation->type) {
+				case 'gmb_create_menu_item':
+				case 'gmb_update_menu_item':
+				case 'gmb_menu_update':
+				case 'gmb_menu_update_' . $this->userID:
+					return $gmb->processMenuUpdate($operation, $data);
+				case 'gmb_create_event':
+				case 'gmb_update_event':
+					return $gmb->syncEventPost($operation, $data);
+				case 'gmb_create_offer':
+				case 'gmb_update_offer':
+					return $gmb->syncOfferPost($operation, $data);
+				case 'gmb_create_post':
+				case 'gmb_update_post':
+					return $gmb->syncStandardPost($operation, $data);
+				case 'gmb_specialHours':
+					return $gmb->setSpecialHours($data['date'], $data['data']);
+				case 'gmb_business_hours':
+					return $gmb->updateBusinessHours($data);
+				case 'gmb_business_info':
+					return $gmb->updateBusinessInfo($data);
+				case 'gmb_update_attributes':
+					return $gmb->updateAttributes($data);
+				default:
+					return $result;
+			}
+		} catch (\Exception $e) {
+			error_log('[GMB] Operation processing error: ' . $e->getMessage());
+			return [
+				'success' => false,
+				'result' => $e->getMessage()
+			];
+		}
+	}
+
+	protected function syncEventPost(object $operation, array $data):\WP_Error|array
+	{
+		$location = $this->getSelectedLocationResourceName();
+
+		if (!$location) {
+			return [
+				'success' => false,
+				'result'  => 'No location Name set.'
+			];
+		}
+
+		$postID = $data['post_id'];
+		$meta = Meta::forPost($postID);
+		$fields = [
+			'start_date',
+			'end_date',
+			'start_time',
+			'end_time',
+			'post_excerpt',
+			'post_title',
+			'post_thumbnail',
+			"_{$this->service_name}_item_id"
+		];
+
+		$data = [
+			'summary'		=> $fields['post_excerpt'],
+			'topic_type'	=> 'EVENT',
+			'event'			=> [
+				'title'	=> $fields['post_title'],
+				'start_date'	=> $this->formatDate($fields['start_date']),
+				'end_date'		=> $this->formatDate(($fields['end_date'] === '') ? $fields['start_date'] : $fields['end_date']),
+				'start_time'	=> $this->formatTime($fields['start_time']),
+				'end_time'		=> $this->formatTime($fields['end_time']),
+			]
+		];
+		if ($fields['post_thumbnail'] !== '') {
+			$url = wp_get_attachment_url($fields['post_thumbnail']);
+			if ($url) {
+				$data['media'] = [
+					['mediaFormat' => 'PHOTO', 'sourceUrl' => $url]
+				];
+			}
+		}
+
+		if ($fields["_{$this->service_name}_item_id"] !== '') {
+			$result =  $this->updatePost($fields["_{$this->service_name}_item_id"], $data);
+		} else {
+			$result = $this->createPost($data);
+			$meta->set("_{$this->service_name}_item_id", $result['name']);
+		}
+
+		return [
+			'success'	=> !empty($result),
+			'result'	=>  $result
+		];
+	}
+
+	protected function syncOfferPost(object $operation, array $data):\WP_Error|array
+	{
+		$location = $this->getSelectedLocationResourceName();
+
+		if (!$location) {
+			return [
+				'success' => false,
+				'result'  => 'No location Name set.'
+			];
+		}
+
+		$postID = $data['post_id'];
+		$meta = Meta::forPost($postID);
+		$fields = [
+			'post_excerpt',
+			'post_title',
+			'coupon_code',
+			'termsConditions',
+			'post_thumbnail',
+			"_{$this->service_name}_item_id"
+		];
+
+		$data = [
+			'summary'		=> $fields['post_excerpt'],
+			'topic_type'	=> 'OFFER',
+			'offer'			=> [
+				'couponCode'	=> ($fields['coupon_code'] === '') ? $fields['post_title'] : $fields['coupon_code'],
+				'redeemOnlineUrl'=> get_the_permalink($postID),
+//				'termsConditions'=> get_bloginfo('terms_condition') TODO: setup terms and Conditions
+			]
+		];
+		if ($fields['post_thumbnail'] !== '') {
+			$url = wp_get_attachment_url($fields['post_thumbnail']);
+			if ($url) {
+				$data['media'] = [
+					['mediaFormat' => 'PHOTO', 'sourceUrl' => $url]
+				];
+			}
+		}
+
+		if ($fields["_{$this->service_name}_item_id"] !== '') {
+			$result =  $this->updatePost($fields["_{$this->service_name}_item_id"], $data);
+		} else {
+			$result = $this->createPost($data);
+			$meta->set("_{$this->service_name}_item_id", $result['name']);
+		}
+
+		return [
+			'success'	=> !empty($result),
+			'result'	=>  $result
+		];
+	}
+
+
+	protected function syncStandardPost(object $operation, array $data):\WP_Error|array
+	{
+		$location = $this->getSelectedLocationResourceName();
+
+		if (!$location) {
+			return [
+				'success' => false,
+				'result'  => 'No location Name set.'
+			];
+		}
+
+		$postID = $data['post_id'];
+		$meta = Meta::forPost($postID);
+		$fields = [
+			'post_excerpt',
+			'post_title',
+			'post_thumbnail',
+			"_{$this->service_name}_item_id"
+		];
+
+		$data = [
+			'summary'		=> $fields['post_excerpt'],
+			'topic_type'	=> 'STANDARD',
+			'call_to_action'			=> [
+				'type'	=> 'learn_more',
+				'url'=> get_the_permalink($postID),
+			]
+		];
+		if ($fields['post_thumbnail'] !== '') {
+			$url = wp_get_attachment_url($fields['post_thumbnail']);
+			if ($url) {
+				$data['media'] = [
+					['mediaFormat' => 'PHOTO', 'sourceUrl' => $url]
+				];
+			}
+		}
+
+		if ($fields["_{$this->service_name}_item_id"] !== '') {
+			$result =  $this->updatePost($fields["_{$this->service_name}_item_id"], $data);
+		} else {
+			$result = $this->createPost($data);
+			$meta->set("_{$this->service_name}_item_id", $result['name']);
+		}
+
+		return [
+			'success'	=> !empty($result),
+			'result'	=>  $result
+		];
+	}
+
+	protected function processMenuUpdate($operation, $data) {
+		try {
+
+			$postID = $data['post_id'];
+			if (!$postID) {
+				return [
+					'success'	=> false,
+					'result'	=> 'No post ID set'
+				];
+			}
+			$post = get_post($postID);
+			$postType = jvbNoBase($post->post_type);
+
+			$args = [
+				'post_type' => jvbCheckBase($postType),
+				'post_status' => 'publish',
+				'posts_per_page' => -1,
+				'meta_query' => [
+					[
+						'key' => BASE.'_keep_synced_' . $this->service_name,
+						'value' => '1',
+						'compare' => '='
+					]
+				]
+			];
+
+			$menu_items = get_posts($args);
+
+			if (empty($menu_items)) {
+				return [
+					'success' => true,
+					'result' => 'No menu items to sync'
+				];
+			}
+
+			$gmb = self::getInstance($data['userID']??null);
+
+			// Build the complete menu structure
+			$menu_data = $gmb->collectMenu($menu_items);
+
+			// Send to Google My Business API
+			$result = $this->updateFoodMenus($menu_data);
+			return [
+				'success' => !is_null($result),
+				'result' => (is_array($result)) ? array_merge($result, ['menu'=>$menu_data]) : ['menu'=>$menu_data],
+			];
+
+		} catch (\Exception $e) {
+			error_log('GMB Menu Update Error: ' . $e->getMessage());
+			return [
+				'success'	=> false,
+				'result'	=> $e->getMessage()
+			];
+		}
+	}
+
+	/***************************************************************
+		API
+	 ***************************************************************/
+	private function getSelectedLocationResourceName(): ?string
+	{
+		if (empty($this->credentials['location'])) {
+			return null;
+		}
+
+		// If already a full resource name, return as-is
+		if (str_starts_with($this->credentials['location'], 'locations/')) {
+			return $this->credentials['location'];
+		}
+
+		// Otherwise, it's a storeCode - need to find the full resource name
+		try {
+			$accounts = $this->getAccounts();
+			foreach ($accounts as $account) {
+				$locations = $this->getLocations($account['name']);
+
+				foreach ($locations as $location) {
+					if ($location['storeCode'] === $this->credentials['location']) {
+						// Auto-migrate: update stored location to use full resource name
+						$this->setSelectedLocation($location['name']);
+						return $location['name']; // This is the full resource name
+					}
+				}
+			}
+		} catch (\Exception $e) {
+			error_log('[GMB] Error finding location resource name: ' . $e->getMessage());
+		}
+
+		return null;
+	}
+	/**
+	 * Update business information
+	 */
+	/**
+	 * @param array $updates
+	 * @return array As expected by OperationQueue.php processOperation
+	 */
+	public function updateBusinessInfo(array $updates): array
+	{
+		/**
+		SEE ALSO: https://developers.google.com/my-business/content/attributes
+
+		MENU EXAMPLE
+		You can add structured menu data, like a food menu, to a location with the use of the PriceList object.
+
+		The following JSON request shows how to publish a breakfast menu to a location. The response contains an instance of the updated Location object.
+
+		HTTP
+
+		PATCH
+		https://mybusiness.googleapis.com/v4/accounts/{accountId}/locations/{locationId}?updateMask=priceLists
+		{
+		"priceLists": [
+		{
+		"priceListId": "Breakfast",
+		"labels": [
+		{
+		"displayName": "Breakfast",
+		"description": "Tasty Google Breakfast",
+		"languageCode": "en"
+		}
+		],
+		"sourceUrl": "http://www.google.com/todays_menu",
+		"sections": [
+		{
+		"sectionId": "entree_menu",
+		"sectionType":"FOOD",
+		"labels": [
+		{
+		"displayName": "Entrées",
+		"description": "Breakfast Entrées",
+		"languageCode": "en"
+		}
+		],
+		"items": [
+		{
+		"itemId": "scramble",
+		"labels": [
+		{
+		"displayName": "Big Scramble",
+		"description": "A delicious scramble filled with Potatoes, Eggs,
+		Bell Peppers, and Sausage",
+		"languageCode": "en"
+		}
+		],
+		"price": {
+		"currencyCode": "USD",
+		"units": "12",
+		"nanos": "200000000"
+		}
+		},
+		{
+		"itemId": "steak_omelette",
+		"labels": [
+		{
+		"displayName": "Steak Omelette",
+		"description": "Three egg omelette with grilled prime rib,
+		fire-roasted bell peppers and onions, saut\u00e9ed mushrooms
+		and melted Swiss cheese",
+		"languageCode": "en"
+		}
+		],
+		"price": {
+		"currencyCode": "USD",
+		"units": "15",
+		"nanos": "750000000"
+		}
+		}
+		]
+		}
+		]
+		}
+		]
+		}
+
+
+		SERVICE DATA
+		If your business offers different service options, you can add structured services data to a location with the use of the PriceList object.
+
+		The following JSON request shows how to publish a service offering to a location. The response contains an instance of the updated Location object.
+
+		HTTP
+
+		PATCH
+		https://mybusiness.googleapis.com/v4/accounts/{accountId}/locations/{locationId}?updateMask=priceLists
+		{
+		"priceLists": [
+		{
+		"priceListId": "Oil Change",
+		"labels": [
+		{
+		"displayName": "Oil Change",
+		"description": "Caseys Qwik Oil Change",
+		"languageCode": "en"
+		}
+		],
+		"sourceUrl": "http://www.google.com/todays_services",
+		"sections": [
+		{
+		"sectionId": "oil_services",
+		"sectionType":”SERVICES”,
+		"labels": [
+		{
+		"displayName": "Services",
+		"description": "Oil Changes",
+		"languageCode": "en"
+		}
+		],
+		"items": [
+		{
+		"itemId": "20-minute-oil-change",
+		"labels": [
+		{
+		"displayName": "20 Minute Oil Change",
+		"description": "Quick oil change and filter service.",
+		"languageCode": "en"
+		}
+		],
+		"price": {
+		"currencyCode": "USD",
+		"units": "30",
+		"nanos": "200000000"
+		}
+		},
+		{
+		"itemId": "full_service_oil_change",
+		"labels": [
+		{
+		"displayName": "Full Service Oil Change",
+		"description": "Quick oil change, filter service, and brake inspection.",
+		"languageCode": "en"
+		}
+		],
+		"price": {
+		"currencyCode": "USD",
+		"units": "45",
+		"nanos": "750000000"
+		}
+		}
+		]
+		}
+		]
+		}
+		]
+		}
+		 **/
+
+		try {
+			$allowed_fields = [
+				'title',           // Business name
+				'phoneNumbers',    // Primary phone
+				'websiteUri',      // Website URL
+				'categories',      // Business categories
+				'storefrontAddress', // Physical address
+				'serviceArea',     // Service area for service businesses
+				'profile',         // Business description
+				'openInfo',        // Opening date info
+				'regularHours',    // Business hours
+				'specialHours',    // Special hours
+			];
+
+			// Filter to only allowed fields
+			$patch_data = array_intersect_key($updates,$allowed_fields);
+
+			$location_name = $this->getSelectedLocationResourceName();
+			if (empty($patch_data)) {
+				throw new \Exception('No valid fields to update');
+			}
+
+			// Create updateMask from the fields being updated
+			$update_mask = implode(',', array_keys($patch_data));
+
+			$response = $this->makeRequest(
+				'PATCH',
+				"/v1/{$location_name}?updateMask={$update_mask}",
+				$patch_data,
+				'base'
+			);
+
+			return [
+				'success'	=> !empty($response),
+				'result'	=> $response
+			];
+
+		} catch (\Exception $e) {
+			$this->logError($e->getMessage(), [
+				'method'	=> 'updateBusinessInfo'
+			]);
+			return [
+				'success'	=> false,
+				'result'	=> $e->getMessage(),
+			];
+		}
+	}
+
+
+	/**
+	 * Update business attributes (amenities, services, etc.)
+	 */
+	/**
+	 * @param string $location_name
+	 * @param array $attributes
+	 * @return array As expected by OperationQueue.php processOperation
+	 */
+	public function updateAttributes(array $attributes): array
+	{
+		try {
+			// Attributes like: wheelchair_accessible, wifi, parking, etc.
+			//TODO: sanitize data
+			$attributes_data = [
+				'attributes' => $attributes
+			];
+			$location_name = $this->getSelectedLocationResourceName();
+
+			$response = $this->makeRequest(
+				'PATCH',
+				"/v1/accounts/{$location_name}/attributes",
+				$attributes_data,
+				'base'
+			);
+
+			return [
+				'success'	=> !empty($response),
+				'result'	=> $response
+			];
+
+		} catch (\Exception $e) {
+			$this->logError($e->getMessage(), [
+				'method'	=> 'updateAttributes'
+			]);
+			return [
+				'success'	=> false,
+				'result'	=> $e->getMessage()
+			];
+		}
+	}
+	/*****************
+	 * POSTS
+	 ****************/
+	/**
+	 * Create a new GMB post
+	 *
+	 * @param string $location_name The location resource name
+	 * @param array $post_data Post content including summary, callToAction, media, etc.
+	 * @return array|null Created post data or null on failure
+	 */
+
+	/**
+	FROM THE DOCS:
+	EVENT POST
+	Notify your customers about the next event at your business with a Post. Your Post for an event includes start and end dates and times, which display prominently on the Post.
+
+	To make a Post to an account associated with a user, use the accounts.locations.localPosts API.
+
+	To create a Post for an authenticated user, use the following:
+
+	$ POST
+	https://mybusiness.googleapis.com/v4/accounts/{accountId}/locations/{locationId}/localPosts
+	{
+	"languageCode": "en-US",
+	"summary": "Come in for our spooky Halloween event!",
+	"event": {
+	"title": "Halloween Spook-tacular!",
+	"schedule": {
+	"startDate": {
+	"year": 2017,
+	"month": 10,
+	"day": 31,
+	},
+	"startTime": {
+	"hours": 9,
+	"minutes": 0,
+	"seconds": 0,
+	"nanos": 0,
+	},
+	"endDate": {
+	"year": 2017,
+	"month": 10,
+	"day": 31,
+	},
+	"endTime": {
+	"hours": 17,
+	"minutes": 0,
+	"seconds": 0,
+	"nanos": 0,
+	},
+	}
+	},
+	"media": [
+	{
+	"mediaFormat": "PHOTO",
+	"sourceUrl": "https://www.google.com/real-image.jpg",
+	}
+	],
+	"topicType": "EVENT"
+	}
+
+	CALL TO ACTION POST
+	Posts with a call to action include a button. The text on the call to action button is determined by the actionType field of the Post. A link to a user-provided URL is added to the button.
+
+	To create a Post with a call to action button, use the following:
+
+	$ POST
+	https://mybusiness.googleapis.com/v4/accounts/{accountId}/locations/{locationId}/localPosts
+	{
+	"languageCode": "en-US",
+	"summary": "Order your Thanksgiving turkeys now!!",
+	"callToAction": {
+	"actionType": "ORDER",
+	"url": "http://google.com/order_turkeys_here",
+	},
+	"media": [
+	{
+	"mediaFormat": "PHOTO",
+	"sourceUrl": "https://www.google.com/real-turkey-photo.jpg",
+	}
+	],
+	"topicType": "OFFER"
+	}
+	Action types
+	The call to action Posts can have different action types that determine the type of call to action Post.
+
+	The following are the supported call to action types:
+
+	Action types
+	BOOK	Creates a Post that prompts a user to book an appointment, table, or something similar.
+	ORDER	Creates a Post that prompts a user to order something.
+	SHOP	Creates a Post that prompts a user to browse a product catalog.
+	LEARN_MORE	Creates a Post that prompts a user to see additional details on a website.
+	SIGN_UP	Creates a Post that prompts a user to register, sign up, or join something.
+	CALL	Creates a Post that prompts a user to call a business.
+
+
+	OFFER POST
+	To create an Offer Post, use the following:
+
+	HTTP
+
+	$ POST
+	https://mybusiness.googleapis.com/v4/accounts/{accountId}/locations/{locationId}/localPosts
+	{
+	"languageCode": "en-US",
+	"summary": "Buy one Google jetpack, get a second one free!!",
+	"offer": {
+	"couponCode": “BOGO-JET-CODE”,
+	"redeemOnlineUrl": “https://www.google.com/redeem”,
+	"termsConditions": “Offer only valid if you can prove you are a time traveler”
+	},
+	"media": [
+	{
+	"mediaFormat": "PHOTO",
+	"sourceUrl": "https://www.google.com/real-jetpack-photo.jpg",
+	}
+	],
+	"topicType": "OFFER"
+	}
+
+	EDIT POST
+	Once a post is created, you can edit it with a PATCH request.
+
+	To edit a Post, use the following:
+
+	HTTP
+
+	$ PATCH
+	https://mybusiness.googleapis.com/v4/accounts/{accountId}/locations/{locationId}/localPosts/{localPostId}?updateMask=summary
+	{
+	"summary": "Order your Christmas turkeys now!!"
+	}
+
+
+	DELETE POSTS
+	After a Post is created, you can delete it with a DELETE request.
+
+	To delete a Post, use the following:
+
+	HTTP
+
+	$ DELETE
+	https://mybusiness.googleapis.com/v4/accounts/{accountId}/locations/{locationId}/localPosts/{localPostId}
+	 **/
+	public function createPost(array $post_data, ?string $location_name = null): ?array
+	{
+		$location_name = (!$location_name) ? $this->getSelectedLocationResourceName() : $location_name;
+		try {
+			// Validate required fields
+			if (empty($post_data['summary'])) {
+				throw new \Exception('Post summary is required');
+			}
+
+			// Build the post object according to API docs
+			$post = [
+				'languageCode' => $post_data['language_code'] ?? 'en-US',
+				'summary' => $post_data['summary'],
+				'topicType' => $post_data['topic_type'] ?? 'STANDARD'
+			];
+
+			// Add optional fields
+			if (!empty($post_data['call_to_action'])) {
+				$post['callToAction'] = [
+					'actionType' => $post_data['call_to_action']['type'] ?? 'LEARN_MORE',
+					'url' => $post_data['call_to_action']['url']
+				];
+			}
+
+			if (!empty($post_data['media'])) {
+				$post['media'] = $post_data['media'];
+			}
+
+			if ($post_data['topic_type'] === 'EVENT' && !empty($post_data['event'])) {
+				$post['event'] = $post_data['event'];
+			}
+
+			if ($post_data['topic_type'] === 'OFFER' && !empty($post_data['offer'])) {
+				$post['offer'] = $post_data['offer'];
+			}
+
+			$response = $this->makeRequest(
+				'POST',
+				"/v4/{$location_name}/localPosts",
+				$post,
+				'posts'
+			);
+
+			$this->cache->invalidate("posts_{$location_name}");
+
+			return $response;
+
+		} catch (\Exception $e) {
+			$this->logError($e->getMessage(), [
+				'method'	=> 'createPost'
+			]);
+			return null;
+		}
+	}
+
+	/**
+	 * Update an existing GMB post
+	 */
+	public function updatePost(string $post_name, array $post_data): bool
+	{
+		try {
+			$update_fields = [];
+			$update_mask = [];
+
+			if (isset($post_data['summary'])) {
+				$update_fields['summary'] = $post_data['summary'];
+				$update_mask[] = 'summary';
+			}
+
+			if (isset($post_data['call_to_action'])) {
+				$update_fields['callToAction'] = $post_data['call_to_action'];
+				$update_mask[] = 'callToAction';
+			}
+
+			if (empty($update_fields)) {
+				return true; // Nothing to update
+			}
+
+			$response = $this->makeRequest(
+				'PATCH',
+				"/v4/{$post_name}?updateMask=" . implode(',', $update_mask),
+				$update_fields,
+				'posts'  // Use the posts API base
+			);
+
+			return !empty($response);
+
+		} catch (\Exception $e) {
+
+			$this->logError($e->getMessage(), [
+				'method'	=> 'updatePost'
+			]);
+			return false;
+		}
+	}
+
+	/**
+	 * Delete a post
+	 */
+	public function deletePost(string $post_name): bool
+	{
+		try {
+			$this->makeRequest('DELETE', "/v1/accounts/{$post_name}", [], 'posts');
+			return true;
+		} catch (\Exception $e) {
+
+			$this->logError($e->getMessage(), [
+				'method'	=> 'deletePost'
+			]);
+			return false;
+		}
+	}
+	/****************
+	 * HOURS
+	 ***************/
+	/**
+	 * Update regular business hours
+	 *
+	 * @param array $hours Array of periods with open/close times per day
+	 * @return array Success status, according to OperationQueue.php
+	 */
+	public function updateBusinessHours(array $hours): array
+	{
+		if(!$this->isSetUp()) {
+			return [];
+		}
+		$location_name = $this->credentials['location'];
+
+		if (empty($location_name)) {
+			error_log('[GMB] ERROR: No location selected or found');
+			throw new \Exception('No location selected. Please select a location first.');
+		}
+
+		try {
+			// Validate and format hours
+			$periods = $this->validateAndFormatHours($hours);
+
+			if (empty($periods)) {
+				error_log('[GMB] ERROR: No valid periods found after validation');
+				throw new \Exception('No valid business hours provided');
+			}
+
+			$update_data = [
+				'regularHours' => [
+					'periods' => $periods
+				]
+			];
+
+			$endpoint = "/v1/{$location_name}?updateMask=regularHours";
+
+			// Make the API request
+			$response = $this->makeRequest(
+				'PATCH',
+				$endpoint,
+				$update_data,
+				'base'
+			);
+
+			$success = $response !== null;
+
+			// Additional validation - check if the response contains the updated hours
+			if ($success && $response) {
+				if (isset($response['regularHours'])) {
+//					error_log('[GMB] SUCCESS: Updated hours confirmed in response: ' . print_r($response['regularHours'], true));
+				} else {
+//					error_log('[GMB] WARNING: No regularHours in response, but API call succeeded');
+//					error_log('[GMB] Full response keys: ' . implode(', ', array_keys($response)));
+				}
+			}
+
+			return [
+				'success' 	=> $success,
+				'result'	=> $response
+			];
+
+		} catch (\Exception $e) {
+			$this->logError($e->getMessage(), [
+				'method'	=> 'updateBusinessHours'
+			]);
+			return [
+				'success' => false,
+				'result'	=> $e->getMessage()
+			];
+		}
+	}
+
+	private function validateAndFormatHours(array $hours): array
+	{
+		$valid_days = ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'];
+		$periods = [];
+
+		foreach ($hours as $day => $times) {
+			$formatted_day = strtoupper($day);
+
+			// Convert day names if needed
+			$day_mapping = [
+				'MON' => 'MONDAY',
+				'TUE' => 'TUESDAY',
+				'TUES' => 'TUESDAY',
+				'WED' => 'WEDNESDAY',
+				'THU' => 'THURSDAY',
+				'THUR' => 'THURSDAY',
+				'THURS' => 'THURSDAY',
+				'FRI' => 'FRIDAY',
+				'SAT' => 'SATURDAY',
+				'SUN' => 'SUNDAY'
+			];
+
+			if (isset($day_mapping[$formatted_day])) {
+				$formatted_day = $day_mapping[$formatted_day];
+			}
+
+			if (!in_array($formatted_day, $valid_days)) {
+				error_log('[GMB] Invalid day: ' . $day . ' (formatted: ' . $formatted_day . ')');
+				continue;
+			}
+
+			// Check if day is open with flexible comparison
+			$is_open = false;
+			if (isset($times['open'])) {
+				// Convert to boolean - handles string "1", integer 1, boolean true, etc.
+				$is_open = (bool)(int)$times['open'];
+			}
+
+			if (!$is_open) {
+				error_log('[GMB] Day ' . $formatted_day . ' is closed (open value: ' . ($times['open'] ?? 'not set') . ')');
+				continue;
+			}
+
+			error_log('[GMB] Day ' . $formatted_day . ' is OPEN');
+
+			$open_time = $this->formatTime($times['time_opens'] ?? '');
+			$close_time = $this->formatTime($times['time_closes'] ?? '');
+
+			if (empty($open_time) || empty($close_time)) {
+				error_log('[GMB] Missing or invalid times for ' . $formatted_day);
+				error_log('[GMB] Raw time data - time_opens: "' . ($times['time_opens'] ?? 'not set') . '", time_closes: "' . ($times['time_closes'] ?? 'not set') . '"');
+				continue;
+			}
+
+			$period = [
+				'openDay' => $formatted_day,
+				'openTime' => $open_time,    // Now a proper Google TimeOfDay object
+				'closeDay' => $formatted_day,
+				'closeTime' => $close_time   // Now a proper Google TimeOfDay object
+			];
+
+			$periods[] = $period;
+		}
+		return $periods;
+	}
+
+	/**
+	 * Set special/temporary hours (for holidays, special events, etc.)
+	 *
+	 * @param string $date
+	 * @param array{open?: float, closed?:float, isClosed?: bool, location?: string} $options
+	 * @return array Operation Queue [ 'success' => true/false, 'result' => $results]
+	 */
+	public function setSpecialHours(string $date, array $options = []):array
+	{
+		// Get the full location resource name
+		$location_name = $this->getSelectedLocationResourceName();
+
+		if (empty($location_name)) {
+			throw new \Exception('No location selected. Please select a location first.');
+		}
+
+		try {
+			// Format the date correctly for Google API
+			$date_obj = [
+				'year' => (int)date('Y', strtotime($date)),
+				'month' => (int)date('n', strtotime($date)),
+				'day' => (int)date('j', strtotime($date))
+			];
+
+			$special_hour_period = [
+				'startDate' => $date_obj,
+				'endDate' => $date_obj, // Same day for single-day special hours
+			];
+
+			if (array_key_exists('isClosed', $options) && $options['isClosed']) {
+				$special_hour_period['closed'] = true;
+			} else {
+				$special_hour_period['closed'] = false;
+
+				// Format times as TimeOfDay objects
+				if (!empty($options['open'])) {
+					$open_time = $this->formatTime($options['open']);
+					if ($open_time) {
+						$special_hour_period['openTime'] = $open_time;
+					}
+				}
+
+				if (!empty($options['close'])) {
+					$close_time = $this->formatTime($options['close']);
+					if ($close_time) {
+						$special_hour_period['closeTime'] = $close_time;
+					}
+				}
+			}
+
+			// Get existing special hours and their periods
+			$existing_special_hours = $location['specialHours'] ?? [];
+			$existing_periods = $existing_special_hours['specialHourPeriods'] ?? [];
+
+			// Add or update the special hour period
+			$found = false;
+			foreach ($existing_periods as &$existing_period) {
+				if (isset($existing_period['startDate']) && $existing_period['startDate'] == $date_obj) {
+					$existing_period = $special_hour_period;
+					$found = true;
+					break;
+				}
+			}
+
+			if (!$found) {
+				$existing_periods[] = $special_hour_period;
+			}
+
+			// Prepare the update data with correct structure
+			$update_data = [
+				'specialHours' => [
+					'specialHourPeriods' => $existing_periods
+				]
+			];
+
+			// Try the PATCH request
+			$response = $this->makeRequest(
+				'PATCH',
+				"/v1/{$location_name}?updateMask=specialHours",
+				$update_data,
+				'base'
+			);
+
+			return [
+				'success'	=> $response !== null,
+				'results'	=> $response
+			];
+
+		} catch (\Exception $e) {
+			$this->logError($e->getMessage(), [
+				'method'	=> 'setSpecialHours'
+			]);
+			return [
+				'success'	=> false,
+				'results'	=> $e->getMessage(),
+			];
+		}
+	}
+	/**
+	 * Clear all special hours
+	 */
+	public function clearSpecialHours(string $location_name): bool
+	{
+		try {
+			$update_data = [
+				'specialHours' => []
+			];
+
+			$response = $this->makeRequest(
+				'PATCH',
+				"/v1/{$location_name}?updateMask=specialHours",
+				$update_data,
+				'base'
+			);
+
+			return $response !== null;
+
+		} catch (\Exception $e) {
+
+			$this->logError($e->getMessage(), [
+				'method'	=> 'clearSpecialHours'
+			]);
+			return false;
+		}
+	}
+	/***************************************************************
+		HOURS
+	 ***************************************************************/
+	/**
+	FROM THE DOCS:
+	UPLOAD FROM URL
+	To upload photos from a URL , make the following call to Media.Create. Use the relevant category as needed.
+
+
+	POST https://mybusiness.googleapis.com/v4/accounts/{accountId}/locations/{locationId}/media
+	{
+	"mediaFormat": "PHOTO",
+	"locationAssociation": {
+	"category": "COVER"
+	},
+	"sourceUrl": “http://example.com/biz/image.jpg",
+	}
+	To upload videos from a URL with the Google My Business API, make the following call to Media.Create:
+
+
+	POST https://mybusiness.googleapis.com/v4/accounts/{accountId}/locations/{locationId}/media
+	{
+	"mediaFormat": "VIDEO",
+	"locationAssociation": {
+	"category": "ADDITIONAL"
+	},
+	"sourceUrl": “http://example.com/biz/video.mp4",
+	}
+	 **/
+	public function uploadImageToGMB(int $imgID, string $category = 'ADDITIONAL'): ?string
+	{
+		if ($imgID === 0) {
+			return null;
+		}
+		if (empty($this->credentials)) {
+			$this->loadCredentials();
+		}
+
+		try {
+			$imgID = $this->getSupportedImage($imgID);
+			$valid_categories = [
+				'COVER', 'PROFILE', 'LOGO', 'EXTERIOR', 'INTERIOR',
+				'PRODUCT', 'AT_WORK', 'FOOD_AND_DRINK', 'MENU',
+				'COMMON_AREA', 'ROOMS', 'TEAMS', 'ADDITIONAL',
+				'FOOD_AND_DRINK'
+			];
+
+			if (!in_array($category, $valid_categories)) {
+				throw new \Exception('Invalid photo category: ' . $category);
+			}
+			$mediaKey = $this->getImageMediaKey($imgID);
+			if ($mediaKey) {
+				return $mediaKey;
+			}
+			$image_url = wp_get_attachment_image_url($imgID, 'full');
+
+			$media_data = [
+				'mediaFormat' => 'PHOTO',
+				'locationAssociation' => [
+					'category' => $category
+				],
+				'sourceUrl' => $image_url
+			];
+			$response = $this->makeRequest(
+				'POST',
+				"/v4/{$this->credentials['account']}/{$this->credentials['location']}/media",
+				$media_data,
+				'posts'
+			);
+
+			if (!is_wp_error($response) && array_key_exists('name', $response)) {
+				$mediaKey = $this->extractMediaKey($response['name']);
+				$this->setImageMediaKey($imgID, $mediaKey);
+				return $mediaKey;
+			}
+			return null;
+		} catch (\Exception $e) {
+			$this->logError($e->getMessage(), [
+				'method' => 'uploadPhotoFromUrl'
+			]);
+			return null;
+		}
+	}
+
+	public function getImageMediaKey(int $imgID):string|false
+	{
+		$mediaKey = get_post_meta($imgID, BASE.'gmb_mediakey', true);
+		if ($mediaKey !== '') {
+			return $this->testGMBImage($mediaKey);
+		}
+		return false;
+	}
+	public function testGMBImage(string $mediaKey):string|false
+	{
+		$existing = $this->getLocationMedia();
+		$exists = array_filter($existing, function ($item) use ($mediaKey) {
+			return $this->extractMediaKey($item['name']) === $mediaKey;
+		});
+		return (empty($exists)) ? false : $mediaKey;
+	}
+	public function setImageMediaKey(int $imgID, string $key):bool
+	{
+		return update_post_meta($imgID, BASE.'gmb_mediakey', $key);
+	}
+	/***************************************************************
+		CREDENTIALS
+	 ***************************************************************/
+	/**
+	 * Get user's GMB accounts
+	 */
+	public function getAccounts(bool $force = false): array
+	{
+		$ttl = 7 * 24 * 60 * 60; // week in seconds
+		$response = $this->getRequest('/v1/accounts', [], 'accounts', $ttl, $force)??[];
+
+		if (isset($response['accounts']) && is_array($response['accounts'])) {
+			return $response['accounts'];
+		}
+
+		if (is_array($response) && isset($response['name'])) {
+			return [$response];
+		}
+
+		return [];
+	}
+
+	/**
+	 * Get reviews for the current location
+	 * @param int $page_size Number of reviews to fetch (max 50)
+	 * @return array|null
+	 */
+	public function getReviews(int $page_size = 5): ?array
+	{
+		$this->ensureInitialized();
+		if (!$this->location) {
+			throw new \Exception('No location selected');
+		}
+		if (!$this->account_id) {
+			throw new \Exception('No account configured');
+		}
+
+		$location = $this->getSelectedLocationResourceName();
+		$account = $this->account_id;
+
+
+		// Check cache first (weekly refresh = 604800 seconds)
+		$cache_key = ['reviews', $location, $page_size];
+		$cached = $this->cache->get($cache_key);
+		if ($cached !== false) {
+			return $cached;
+		}
+
+		try {
+			// Reviews endpoint from My Business Account Management API
+			$response = $this->getRequest(
+				"/{$account}/{$location}/reviews",
+				[
+					'orderBy' => 'updateTime desc'
+				],
+				'v4'
+			);
+
+			$reviews = $response ?? [];
+
+			// Cache for 1 week (604800 seconds)
+			$this->cache->set($cache_key, $reviews, WEEK_IN_SECONDS);
+
+			return $reviews;
+
+		} catch (\Exception $e) {
+			$this->logError($e->getMessage(), [
+				'method' => 'getReviews'
+			]);
+			return null;
+		}
+	}
+
+	/**
+	 * Get the URL to view all Google reviews for the current location
+	 * @return string|null The reviews viewing URL or null if not available
+	 */
+	public function getReviewsViewUrl(): ?string
+	{
+		$this->ensureInitialized();
+		try {
+			$location = $this->getLocation();
+
+			if (empty($location)) {
+				return null;
+			}
+
+			// Prefer maps URL as it shows all reviews directly
+			if (!empty($location['metadata']['mapsUrl'])) {
+				return $location['metadata']['mapsUrl'];
+			}
+
+			// Fallback: construct from Place ID
+			if (!empty($location['metadata']['placeId'])) {
+				return 'https://search.google.com/local/reviews?placeid=' .
+					urlencode($location['metadata']['placeId']);
+			}
+
+			return null;
+
+		} catch (\Exception $e) {
+			$this->logError('Failed to get reviews view URL: ' . $e->getMessage(), [
+				'method' => 'getReviewsViewUrl'
+			]);
+			return null;
+		}
+	}
+
+	/**
+	 * Get the URL to leave a review for the current location
+	 * @return string|null The review URL or null if not available
+	 */
+	public function getReviewUrl(): ?string
+	{
+		$this->ensureInitialized();
+		try {
+			$location = $this->getLocation();
+
+			if (empty($location)) {
+				return null;
+			}
+
+			// Try to use Place ID for write review
+			if (!empty($location['metadata']['placeId'])) {
+				return 'https://search.google.com/local/writereview?placeid=' .
+					urlencode($location['metadata']['placeId']);
+			}
+
+			// Fallback to maps URL
+			if (!empty($location['metadata']['mapsUrl'])) {
+				return $location['metadata']['mapsUrl'] . '/reviews';
+			}
+
+			return null;
+
+		} catch (\Exception $e) {
+			$this->logError('Failed to get review URL: ' . $e->getMessage(), [
+				'method' => 'getReviewUrl'
+			]);
+			return null;
+		}
+	}
+
+	/**
+	 * Get locations for an account (with persistent storage)
+	 * Allowed Fields: https://developers.google.com/my-business/content/location-data#list_of_all_supported_filter_fields
+	 */
+	public function getLocations(?string $account_name = null, bool $force = false): array
+	{
+		if (!$this->isSetUp()) {
+			return [];
+		}
+
+		$account_name = $account_name ?: $this->account_id;
+
+		if (empty($account_name) || !str_starts_with($account_name, 'accounts/')) {
+			return [];
+		}
+		$params = ['readMask' => $this->readMask];
+		$ttl = 7 * 24 * 60 * 60; // Week in seconds
+
+		try {
+			$response = $this->getRequest("/v1/{$account_name}/locations", $params,'accounts', $ttl, $force);
+			return isset($response['locations']) ? $response['locations'] : [];
+
+		} catch (\Exception $e) {
+			error_log('[GMB] Error getting locations for ' . $account_name . ': ' . $e->getMessage());
+			return [];
+		}
+	}
+
+	public function handleRefreshAccessToken():WP_Error|array
+	{
+		if ($this->refreshOAuthToken()) {
+			return [
+				'success'	=> true,
+				'message'	=> 'Successfully refreshed OAuth Token'
+			];
+		}
+		return new WP_Error('failure', 'Failed to Refresh token');
+	}
+
+	/**
+	 * Manually refresh accounts and locations data from API
+	 */
+	public function refreshStoredData(): array
+	{
+		try {
+			// Fetch fresh accounts data from API
+			$accounts = $this->getAccounts(true);
+			if (!empty($accounts)) {
+				// Fetch and store locations for each account
+				$locationsProcessed = 0;
+				$accountsProcessed = count($accounts);
+				foreach ($accounts as $account) {
+					$locations = $this->getLocations($account['name'], true);
+					if (!empty($locations)) {
+						$locationsProcessed += count($locations);
+					}
+				}
+
+				return [
+					'success' => true,
+					'accounts' => $accountsProcessed,
+					'locations' => $locationsProcessed,
+					'message' => sprintf(
+						'Successfully refreshed %d accounts with %d total locations',
+						$accountsProcessed,
+						$locationsProcessed
+					)
+				];
+			}
+
+			return [
+				'success' => false,
+				'message' => 'No accounts found'
+			];
+
+		} catch (\Exception $e) {
+			error_log('[GMB] Error refreshing stored data: ' . $e->getMessage());
+			return [
+				'success' => false,
+				'message' => 'Error: ' . $e->getMessage()
+			];
+		}
+	}
+
+	/**
+	 * Get a specific location details
+	 */
+	public function getLocation(?string $location_name = null, bool $force = false):?array
+	{
+		if (!$this->isSetUp()) {
+			return null;
+		}
+		$location_name = $location_name?:$this->credentials['location'];
+		if (empty($location_name)) {
+			error_log('[GMB] No location name provided for getLocation');
+			return null;
+		}
+
+		$params = [
+			'readMask' => $this->readMask
+		];
+		$endpoint = "/v1/{$location_name}?".http_build_query($params);
+
+		$location = $this->getRequest($endpoint, [], 'base', 'moderate', $force);
+
+		return $location??null;
+	}
+
+	/**
+	 * Get the currently selected location
+	 */
+	public function getSelectedLocation(): ?string
+	{
+		return $this->credentials['location'];
+	}
+
+	/**
+	 * Set the selected location
+	 */
+	public function setSelectedLocation(string $location): bool
+	{
+		$this->credentials['location'] = $location;
+
+		if (preg_match('/accounts\/([^\/]+)\//', $location, $matches)) {
+			$this->account_id = 'accounts/' . $matches[1];
+		}
+
+		$credentials = $this->credentials;
+		$credentials['location'] = $location;
+		$credentials['account'] = $this->account_id;
+
+
+		return $this->saveCredentials($credentials)['success'];
+	}
+
+	/*************************************************************
+	RENDER
+	 *************************************************************/
+	/**
+	 * Render business list for connected users
+	 */
+	private function renderBusinessList(): void
+	{
+		try {
+			$accounts = $this->getAccounts();
+			if (empty($accounts)) {
+				echo '<p>No business accounts found.</p>';
+				return;
+			}
+			?>
+			<div class="gmb-business-list">
+				<h4>Your Business Locations</h4>
+				<?php
+				foreach ($accounts as $account) {
+					$locations = $this->getLocations($account['name']);
+					foreach ($locations as $location) {
+						?>
+						<div class="business-location">
+							<strong><?php echo esc_html($location['title']); ?></strong>
+							<?php if ($this->isSelectedLocation($location)): ?>
+								<span class="selected-badge">✓ Selected</span>
+							<?php endif; ?>
+						</div>
+						<?php
+					}
+				}
+				?>
+			</div>
+			<?php
+		} catch (\Exception $e) {
+			echo '<p>Error loading business locations.</p>';
+		}
+	}
+
+	private function isSelectedLocation(array $location): bool
+	{
+		if (empty($this->credentials['location'])) {
+			return false;
+		}
+
+		return ($location['name'] === $this->credentials['location']);
+	}
+
+	/***********************************************************
+		UTILITY
+	 ***********************************************************/
+	/**
+	 * Helper method to strip HTML tags from text
+	 * Preserves plain text content while removing all HTML formatting
+	 */
+	protected function stripHtmlTags(string $text): string
+	{
+		// Remove HTML tags
+		$text = wp_strip_all_tags($text);
+
+		// Decode HTML entities
+		$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+
+		// Clean up whitespace
+		$text = preg_replace('/\s+/', ' ', $text);
+
+		// Trim
+		return trim($text);
+	}
+	/**
+	 * Format date for GMB API
+	 */
+	protected function formatDate(string $date): array
+	{
+		$timestamp = strtotime($date);
+		return [
+			'year' => (int)date('Y', $timestamp),
+			'month' => (int)date('n', $timestamp),
+			'day' => (int)date('j', $timestamp)
+		];
+	}
+	/**
+	 * Format time for Google My Business API (HH:MM format)
+	 */
+	private function formatTime(string $time): ?array
+	{
+		if (empty($time)) {
+			return null;
+		}
+
+		// Remove any extra whitespace
+		$time = trim($time);
+
+		// Handle different time formats and convert to 24-hour format
+		if (preg_match('/^(\d{1,2}):(\d{2})(?:\s*(AM|PM))?$/i', $time, $matches)) {
+			$hour = (int)$matches[1];
+			$minute = (int)$matches[2];
+			$ampm = isset($matches[3]) ? strtoupper($matches[3]) : '';
+
+			// Convert 12-hour to 24-hour format if needed
+			if ($ampm === 'PM' && $hour !== 12) {
+				$hour += 12;
+			} elseif ($ampm === 'AM' && $hour === 12) {
+				$hour = 0;
+			}
+
+			// Validate hour and minute ranges
+			if ($hour >= 0 && $hour <= 23 && $minute >= 0 && $minute <= 59) {
+				return [
+					'hours' => $hour,
+					'minutes' => $minute
+				];
+			}
+		}
+
+		error_log('[GMB] Invalid time format: ' . $time);
+		return null;
+	}
+
+
+
+	/*****************************************************************************
+	 *
+	 * OAUTH IMPLEMENTATION
+	 *
+	*****************************************************************************/
+	private function getScopes(): string
+	{
+		return implode(' ', [
+			'https://www.googleapis.com/auth/business.manage',
+			'https://www.googleapis.com/auth/userinfo.email',
+			'https://www.googleapis.com/auth/userinfo.profile'
+		]);
+	}
+	/**
+	 * Handle account selection
+	 */
+	public function handleSelectAccount($data = null): array
+	{
+		try {
+			$account_name = null;
+
+			// Extract account name from various sources
+			if (is_array($data)) {
+				$account_name = $data['account'] ?? $data['account'] ?? null;
+			} elseif (isset($_POST['account'])) {
+				$account_name = sanitize_text_field($_POST['account']);
+			}
+
+			if (empty($account_name)) {
+				return [
+					'success'	=> false,
+					'message'	=> 'No Account Selected'
+				];
+			}
+
+			// Get account details
+			$accounts = $this->getAccounts();
+			$selected_account = null;
+
+			foreach ($accounts as $account) {
+				if ($account['name'] === $account_name) {
+					$selected_account = $account;
+					break;
+				}
+			}
+
+			if (!$selected_account) {
+				throw new \Exception('Invalid account selected');
+			}
+
+			// Update credentials with selected account
+			$this->credentials['account'] = $account_name;
+			$this->credentials['selected_account_id'] = $selected_account['accountId'] ?? '';
+
+			$this->saveCredentials($this->credentials);
+
+			return [
+				'success' => true,
+				'message' => 'Account selected successfully',
+				'reload' => true
+			];
+
+		} catch (\Exception $e) {
+			$this->logError('Failed to select account', [
+				'error' => $e->getMessage(),
+				'data' => $data
+			]);
+
+			return [
+				'success' => false,
+				'message' => $e->getMessage()
+			];
+		}
+	}
+
+	/**
+	 * Handle location selection
+	 */
+	public function handleSelectLocation($data = null): array
+	{
+		try {
+			$location_name = null;
+
+			// Extract location name from various sources
+			if (is_array($data)) {
+				$location_name = $data['location'] ?? $data['location'] ?? null;
+			} elseif (isset($_POST['location'])) {
+				$location_name = sanitize_text_field($_POST['location']);
+			}
+
+			if (empty($location_name)) {
+				throw new \Exception('No location selected');
+			}
+
+			// Verify this is a valid location for the selected account
+			if (empty($this->credentials['account'])) {
+				throw new \Exception('No account selected. Please select an account first.');
+			}
+
+			$locations = $this->getLocations($this->credentials['account']);
+			$valid_location = false;
+
+			foreach ($locations as $location) {
+				if ($location['name'] === $location_name) {
+					$valid_location = true;
+					$this->credentials['location_id'] = $location['locationId'] ?? '';
+					break;
+				}
+			}
+
+			if (!$valid_location) {
+				throw new \Exception('Invalid location selected');
+			}
+
+			// Update credentials with selected location
+			$this->credentials['location'] = $location_name;
+
+			// Save updated credentials
+			$this->saveCredentials($this->credentials);
+
+			// Initialize with new location
+			$this->credentials['location'] = $location_name;
+
+			return [
+				'success' => true,
+				'message' => 'Location selected successfully',
+				'reload' => false  // No need to reload for location selection
+			];
+
+		} catch (\Exception $e) {
+			$this->logError('Failed to select location', [
+				'error' => $e->getMessage(),
+				'data' => $data
+			]);
+
+			return [
+				'success' => false,
+				'message' => $e->getMessage()
+			];
+		}
+	}
+
+	/**
+	 * Check OAuth status
+	 */
+	public function checkOAuthStatus(): array
+	{
+		try {
+			$valid = $this->isOAuthValid();
+
+			return [
+				'success' => true,
+				'valid' => $valid,
+				'expires_at' => $this->credentials['expires_at'] ?? null,
+				'has_refresh_token' => !empty($this->credentials['refresh_token'])
+			];
+
+		} catch (\Exception $e) {
+			return [
+				'success' => false,
+				'valid' => false,
+				'message' => $e->getMessage()
+			];
+		}
+	}
+
+
+	/********************************************************************
+	 *
+	 * ADMIN UI
+	 *
+	********************************************************************/
+	/**
+	 * Updated renderOAuthConnectedOptions for better UI
+	 */
+	protected function renderOAuthConnectedOptions(): void
+	{
+		if (!$this->isSetUp()) {
+			return;
+		}
+
+		// Check OAuth status
+		$oauth_valid = $this->isOAuthValid();
+
+		if (!$oauth_valid) {
+			?>
+			<div class="oauth-status">
+				<span class="status-invalid">⚠️ OAuth token expired - please reconnect</span>
+			</div>
+			<?php
+		}
+
+		// Show account selection
+		$accounts = $this->getAccounts();
+		if (!empty($accounts)) {
+			?>
+			<div class="form-field" data-service="gmb">
+				<label for="gmb_account">Google Business Account</label>
+				<select name="account"
+						id="gmb_account"
+						class="jvb-ajax-update"
+						data-action="select_account">
+					<option value="">Select an account...</option>
+					<?php foreach ($accounts as $account): ?>
+						<option value="<?php echo esc_attr($account['name']); ?>"
+							<?php selected($credentials['account'] ?? '', $account['name']); ?>>
+							<?php echo esc_html($account['accountName'] ?? $account['name']); ?>
+						</option>
+					<?php endforeach; ?>
+				</select>
+			</div>
+			<?php
+
+			// Show location selection if account is selected
+			if (!empty($this->credentials['account'])) {
+				$locations = $this->getLocations($this->credentials['account']);
+
+				if (!empty($locations)) {
+					?>
+					<div class="form-field location-field visible" data-service="gmb">
+						<label for="gmb_location">Business Location</label>
+						<select name="location"
+								id="gmb_location"
+								class="jvb-ajax-update"
+								data-action="select_location">
+							<option value="">Select a location...</option>
+							<?php foreach ($locations as $location): ?>
+								<option value="<?php echo esc_attr($location['name']); ?>"
+									<?php selected($this->credentials['location'] ?? '', $location['name']); ?>>
+									<?php echo esc_html($location['title'] ?? $location['name']); ?>
+								</option>
+							<?php endforeach; ?>
+						</select>
+					</div>
+					<?php
+				}
+			}
+
+			// Show current selection status
+			if (!empty($this->credentials['location'])) {
+				?>
+				<div class="connection-status connected">
+					✅ Connected to: <?php echo esc_html($this->credentials['location']); ?>
+				</div>
+				<?php
+			}
+		} else {
+			?>
+			<div class="notice notice-warning">
+				<p>No Google Business accounts found. Please ensure your Google account has access to Google My Business.</p>
+			</div>
+			<?php
+		}
+	}
+
+	public function getServiceDescription(): string
+	{
+		return "Manage your Google My Business listings, posts, hours, and reviews.";
+	}
+
+	/*****************************************************************************
+	 *
+	 * FOOD MENU IMPLEMENTATION
+	 *
+	 *****************************************************************************/
+
+	/**
+	 * Get food menus for the current location
+	 */
+	public function getFoodMenus(): ?array
+	{
+		if (!$this->credentials['location']) {
+			throw new \Exception('No location selected. Please select a location first.');
+		}
+
+		$response = $this->getRequest(
+			"/v4/{$this->credentials['location']}/foodMenu",
+			[],
+			'posts'
+		);
+		return $response['foodMenus'] ?? [];
+	}
+
+	/**
+	 * Update food menus for the current location
+	 */
+	public function updateFoodMenus(array $menus): bool
+	{
+		if (!$this->credentials['location']) {
+			throw new \Exception('No location selected. Please select a location first.');
+		}
+
+		try {
+			$food_menus = ['menus' => $menus];
+
+			$endpoint = "/{$this->credentials['account']}/{$this->credentials['location']}/foodMenus";
+
+			// Use dedicated Food Menu API
+			$response = $this->makeRequest(
+				'PATCH',
+				$endpoint,
+				$food_menus,
+				'v4'
+			);
+
+			$this->logDebug('[GMB]Finished updating food menu', [
+				'method'	=> 'updateFoodMenus',
+				'response'	=> $response,
+			]);
+			return !empty($response);
+
+		} catch (\Exception $e) {
+			$this->logError($e->getMessage(), [
+				'method'	=> 'updateFoodMenus'
+			]);
+			return false;
+		}
+	}
+
+	/**
+	 * Upload menu images and return a map of identifiers to mediaKeys
+	 */
+	protected function uploadAndMapMenuImages(array $menus): array
+	{
+		$mediaKeyMap = [];
+
+		// Process all menu items for images
+		foreach ($menus as $menu) {
+			foreach ($menu['sections'] ?? [] as $section) {
+				// Upload section images if needed (though API doesn't support section images yet)
+				if (!empty($section['image']) && is_numeric($section['image'])) {
+					$mediaKey = $this->uploadImageToGMB($section['image'], 'FOOD_AND_DRINK');
+					if ($mediaKey) {
+						$mediaKeyMap['section_' . $section['id']] = $mediaKey;
+					}
+				}
+
+				// Process menu items
+				foreach ($section['items'] ?? [] as $item) {
+					// Upload featured image
+					if (!empty($item['post_id'])) {
+						$featured_image_id = get_post_thumbnail_id($item['post_id']);
+						if ($featured_image_id) {
+							$mediaKey = $this->uploadImageToGMB($featured_image_id, 'FOOD_AND_DRINK');
+							if ($mediaKey) {
+								$mediaKeyMap['item_' . $item['post_id']] = $mediaKey;
+							}
+						}
+
+						// Upload gallery images if available
+						if (!empty($item['gallery_ids'])) {
+							foreach ($item['gallery_ids'] as $gallery_id) {
+								$mediaKey = $this->uploadImageToGMB($gallery_id, 'FOOD_AND_DRINK');
+								if ($mediaKey) {
+									$mediaKeyMap['gallery_'.$gallery_id] = $mediaKey;
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+
+		return $mediaKeyMap;
+	}
+
+	/**
+	 * Extract mediaKey from the full media resource name
+	 */
+	protected function extractMediaKey(string $mediaResourceName): string
+	{
+		// Extract mediaKey from format: accounts/{accountId}/locations/{locationId}/media/{mediaKey}
+		$parts = explode('/media/', $mediaResourceName);
+		return isset($parts[1]) ? $parts[1] : '';
+	}
+
+	/**
+	 * Get all media items for the current location
+	 */
+	public function getLocationMedia(): array
+	{
+		if (!$this->credentials['location']) {
+			throw new \Exception('No location selected. Please select a location first.');
+		}
+
+		try {
+			$response = $this->getRequest(
+				"/{$this->credentials['account']}/{$this->credentials['location']}/media",
+				[],
+				'v4'
+			);
+
+			return $response['mediaItems'] ?? [];
+		} catch (\Exception $e) {
+			$this->logError('Failed to get location media: ' . $e->getMessage());
+			return [];
+		}
+	}
+
+	public function handleGetAllLocations():WP_Error|array
+	{
+		try {
+			// Check if we should force refresh
+			$force = isset($_POST['force']) && $_POST['force'] === 'true';
+
+			// Get accounts (use cache unless forced)
+			$accounts = $this->getAccounts($force);
+			$all_locations = [];
+
+			foreach ($accounts as $account) {
+				// Get locations for each account (use cache unless forced)
+				$locations = $this->getLocations($account['name'], $force);
+				$all_locations = array_merge($all_locations, $locations);
+			}
+
+			return [
+				'success'	=> true,
+				'locations' => $all_locations,
+				'cached' => !$force
+			];
+
+		} catch (\Exception $e) {
+			return new WP_Error('failure', 'Something went wrong: '.$e->getMessage());
+		}
+	}
+
+	/**
+	 * AJAX handler for updating selected location
+	 */
+	public function handleUpdateLocation($data = null): array
+	{
+		try {
+			// Handle both AJAX and REST API calls
+			$selected_account = null;
+
+			if (is_array($data)) {
+				$selected_account = $data['account'] ?? $data['location'] ?? null;
+			} elseif (isset($_POST['account'])) {
+				$selected_account = sanitize_text_field($_POST['account']);
+			}
+
+			if (empty($selected_account)) {
+				throw new \Exception('No account selected');
+			}
+
+			// Save the selected account
+			$credentials = $this->credentials;
+			$credentials['account'] = $selected_account;
+
+			// Get locations for this account
+			$locations = $this->getLocations($selected_account);
+
+			// Auto-select first location if only one exists
+			if (count($locations) === 1) {
+				$credentials['location'] = $locations[0]['name'];
+			}
+
+			$this->saveCredentials($credentials);
+
+			return [
+				'success' => true,
+				'message' => 'Account selected successfully',
+				'locations' => $locations,
+				'reload' => true  // Trigger page refresh to show location dropdown
+			];
+
+		} catch (\Exception $e) {
+			$this->logError('Failed to update location', [
+				'error' => $e->getMessage(),
+				'data' => $data
+			]);
+
+			return [
+				'success' => false,
+				'message' => $e->getMessage()
+			];
+		}
+	}
+
+	public function clearStoredData(): bool
+	{
+		try {
+			// Use the static method to clear the entire cache group
+			$this->cache->flush();
+			return true;
+
+		} catch (\Exception $e) {
+			error_log('[GMB] Error clearing stored data: ' . $e->getMessage());
+			return false;
+		}
+	}
+
+	public function getPriceLists(): ?array
+	{
+		if (!$this->credentials['location']) {
+			throw new \Exception('No location selected. Please select a location first.');
+		}
+
+		try {
+			$location_data = $this->getLocation($this->credentials['location']);
+			return $location_data['priceLists'] ?? [];
+		} catch (\Exception $e) {
+
+			$this->logError($e->getMessage(), [
+				'method'	=> 'getPriceLists'
+			]);
+			return null;
+		}
+	}
+
+	/**
+	 * Get insights/analytics
+	 */
+	public function getInsights(string $location_name, string $start_date, string $end_date): ?array
+	{
+//		From the Docs:
+//		Basic insights
+//		Retrieves basic insights for a given list of locations. Use the accounts.locations.reportInsights API to return the insights that are associated with a location.
+//
+//			To return the basic insights associated with a location, use the following:
+//
+//		HTTP
+//
+//		POST
+//		https://mybusiness.googleapis.com/v4/accounts/{accountId}/locations:reportInsights
+//		{
+//			"locationNames": [
+//			"accounts/{accountId}/locations/{locationId}"
+//		],
+//		  "basicRequest": {
+//			"metricRequests": [
+//					 {
+//						 "metric": "QUERIES_DIRECT"
+//					 },
+//					 {
+//						 "metric": "QUERIES_INDIRECT"
+//					 }
+//				  ],
+//				  "timeRange": {
+//				"startTime": "2016-10-12T01:01:23.045123456Z",
+//					   "endTime": "2017-01-10T23:59:59.045123456Z"
+//				  }
+//			}
+//		}
+
+			$params = [
+				'dailyMetrics' => 'BUSINESS_IMPRESSIONS_DESKTOP_MAPS,BUSINESS_IMPRESSIONS_DESKTOP_SEARCH,BUSINESS_IMPRESSIONS_MOBILE_MAPS,BUSINESS_IMPRESSIONS_MOBILE_SEARCH,BUSINESS_DIRECTION_REQUESTS,CALL_CLICKS,WEBSITE_CLICKS',
+				'dailyRange.startDate.year' => date('Y', strtotime($start_date)),
+				'dailyRange.startDate.month' => date('n', strtotime($start_date)),
+				'dailyRange.startDate.day' => date('j', strtotime($start_date)),
+				'dailyRange.endDate.year' => date('Y', strtotime($end_date)),
+				'dailyRange.endDate.month' => date('n', strtotime($end_date)),
+				'dailyRange.endDate.day' => date('j', strtotime($end_date))
+			];
+
+		return $this->getRequest(
+			"/v1/{$location_name}:fetchMultiDailyMetricsTimeSeries?" . http_build_query($params),
+			$params,
+			'performance'
+		);
+	}
+
+	/**
+	 * Helper: Format hours for GMB API
+	 */
+	private function formatHoursPeriods(array $hours): array
+	{
+		$periods = [];
+		$days = ['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY'];
+
+		foreach ($hours as $day => $times) {
+			if (empty($times['open']) || empty($times['close'])) {
+				continue;
+			}
+
+			$periods[] = [
+				'openDay' => $days[$day] ?? $day,
+				'openTime' => $times['open'], // Format: "09:00"
+				'closeDay' => $days[$day] ?? $day,
+				'closeTime' => $times['close'] // Format: "17:00"
+			];
+		}
+
+		return $periods;
+	}
+
+
+	/**
+	 * HELPER: Easy interface method for creating a simple post
+	 * JVB()->connect('gmb')->createSimplePost("Check out our new menu!", "https://example.com/menu")
+	 */
+	public function createSimplePost(string $message, ?string $cta_url = null, string $cta_type = 'LEARN_MORE'): ?array
+	{
+		$location_name = $this->getSelectedLocationResourceName();
+
+		if (empty($location_name)) {
+			throw new \Exception('No location selected. Please select a location first.');
+		}
+
+		$post_data = [
+			'summary' => $message,
+			'topic_type' => 'STANDARD'
+		];
+
+		if ($cta_url) {
+			$post_data['call_to_action'] = [
+				'type' => $cta_type,
+				'url' => $cta_url
+			];
+		}
+
+		return $this->createPost($location_name, $post_data);
+	}
+
+	/**
+	 * HELPER: Easy interface method for updating basic business info
+	 * JVB()->connect('gmb')->updateInfo(['title' => 'New Business Name', 'websiteUri' => 'https://newsite.com'])
+	 */
+	public function updateInfo(array $updates): bool
+	{
+		$location_name = $this->getSelectedLocationResourceName();
+
+		if (empty($location_name)) {
+			throw new \Exception('No location selected. Please select a location first.');
+		}
+
+		return $this->updateBusinessInfo($location_name, $updates);
+	}
+
+
+	/**
+	 * HELPER: Easy interface for clearing special hours
+	 * JVB()->connect('gmb')->clearAllSpecialHours()
+	 */
+	public function clearAllSpecialHours(): bool
+	{
+		$location_name = $this->getSelectedLocationResourceName();
+
+		if (empty($location_name)) {
+			throw new \Exception('No location selected. Please select a location first.');
+		}
+
+		return $this->clearSpecialHours($location_name);
+	}
+
+
+	protected function mappedMenuFields(string $postType):array
+	{
+		/**
+		 * @param array $fields
+		 * @param string $fields.name Thing
+		 */
+		return apply_filters(
+			'jvbGMBFields',
+		[
+			'name'			=> 'post_title',
+			'price'			=> 'price',
+			'description'	=> 'post_excerpt',
+			'nutritionFacts'=> 'nutritionFacts',
+			'servesNumPeople'=>'servesNumPeople',
+			'portionSize'	=> 'portionSize',
+			'preparationMethods' => 'preparationMethods',
+			'cuisines'		=> 'cuisines',
+			'spiciness'		=> 'spiciness',
+			'allergen'		=> 'allergen',
+			'dietaryRestriction'=>'dietaryRestriction'
+		],
+		jvbNoBase($postType));
+	}
+
+
+
+	/**
+	 * Build menu structure from WordPress posts
+	 */
+	protected function collectMenu(array $menu_items): array
+	{
+
+		$defaultMeta = Meta::forOptions($this->userID.'_integrations');
+		$defaults = ['menu_name', 'menu_description', 'default_section', 'cuisines', 'source_url', 'language', 'default_currency'];
+		$defaults = $defaultMeta->getAll($defaults);
+
+		// Group items by section
+		$sections_map = [];
+		foreach ($menu_items as $item) {
+
+			$gmb_item = $this->buildMenuItem($item, $defaults);
+
+			// Get section for this item
+			$categories = wp_get_post_terms($item->ID, BASE.'section');
+			$section_slug = !empty($categories) ?
+				$categories[0]->slug :
+				'menu-items';
+			$section_name = !empty($categories) ?
+				$categories[0]->name :
+				($defaults['default_section'] ?? 'Menu Items');
+
+			if (!isset($sections_map[$section_slug])) {
+				$sections_map[$section_slug] = [
+					'labels'	=> [
+						'displayName' => $section_name,
+						'languageCode' => $defaults['language'] ?? 'en',
+					],
+					'items' => [],
+				];
+				if (!empty($section['description'])) {
+					$sections_map[$section_slug]['labels']['description'] = $this->stripHtmlTags($section['description']);
+				}
+			}
+
+			$sections_map[$section_slug]['items'][] = $gmb_item;
+		}
+
+		$gmb_sections = $this->getMenuSectionsOrder($sections_map);
+
+		// Build complete menu
+		$gmb_menu = [
+			'cuisines' => $this->getMenuCuisines($menu_items),
+			'labels'	=> [
+				'displayName' => $defaults['menu_name'] ?? 'Our Menu',
+				'languageCode' => $defaults['language'] ?? 'en',
+			],
+			'sections' => $gmb_sections
+		];
+
+		if (array_key_exists('menu_description', $defaults)) {
+			$gmb_menu['labels']['description'] = $this->stripHtmlTags($defaults['menu_description']);
+		}
+
+		// Add source URL if configured
+		if (!empty($defaults['source_url'])) {
+			$gmb_menu['source_url'] = $defaults['source_url'];
+		}
+
+		return [$gmb_menu]; // GMB expects an array of menus
+	}
+
+	protected function buildMenuItem(\WP_Post $item, array $defaults):array
+	{
+		$meta = Meta::forPost($item->ID);
+		$fields = $this->mappedMenuFields($item->post_type);
+		$values = $meta->getAll(array_values($fields));
+
+
+		// Build GMB-formatted item
+		$gmb_item = [
+			'labels'	=> [
+				'displayName' => $item->post_title,
+				'description' => !empty($item->post_excerpt) ?
+					$this->stripHtmlTags($item->post_excerpt) :
+					$this->stripHtmlTags(wp_trim_words($item->post_content, 20)),
+				'languageCode' => $defaults['language'] ?? 'en'
+			],
+			'attributes'	=> []
+		];
+
+		// Process mapped fields
+		foreach ($fields as $googleField => $wpField) {
+			if (in_array($wpField, ['post_title', 'post_excerpt'])) {
+				continue;
+			}
+
+			$value = $values[$wpField] ?? null;
+			if (empty($value)) {
+				continue;
+			}
+
+			// Process field based on type
+			$processed = $this->processMenuField($googleField, $value, $values);
+			if ($processed !== null) {
+				$gmb_item['attributes'] = array_merge($gmb_item['attributes'], $processed);
+			}
+		}
+		$featuredImage = get_post_thumbnail_id($item->ID);
+		$mediaKey = $this->uploadImageToGMB($featuredImage, 'FOOD_AND_DRINK');
+		if ($mediaKey) {
+			$gmb_item['attributes'] = array_merge($gmb_item['attributes'], ['mediaKeys' => $mediaKey]);
+		}
+		if (empty($gmb_item['attributes'])) {
+			unset($gmb_item['attributes']);
+		}
+
+		return $gmb_item;
+	}
+
+	/**
+	 * Process a single menu field value
+	 */
+	protected function processMenuField(string $fieldName, $value, array $allValues): ?array
+	{
+		switch ($fieldName) {
+			case 'price':
+				if (is_numeric($value)) {
+					return [
+						'price' => [
+							'currencyCode'	=> $allValues['price_currency'] ?? 'CAD',
+							'units' => intval($value),
+							'nanos' => (($value - intval($value)) * 1000000000)
+						]
+					];
+				}
+				break;
+
+			case 'nutritionFacts':
+				if (is_array($value)) {
+					return ['nutritionFacts' => $this->processNutritionData($value)];
+				}
+				break;
+
+			case 'allergen':
+				$allergens = $this->processListField($value);
+				return $allergens ? ['allergens' => $allergens] : null;
+
+			case 'dietaryRestriction':
+				$restrictions = $this->processListField($value);
+				return $restrictions ? ['dietaryRestrictions' => $restrictions] : null;
+
+			case 'servesNumPeople':
+				return ['servesNumPeople' => intval($value)];
+
+			case 'portionSize':
+				return ['portionSize' => $value];
+
+			case 'preparationMethods':
+				$methods = $this->processListField($value);
+				return $methods ? ['preparationMethods' => $methods] : null;
+
+			case 'cuisines':
+				$cuisines = $this->processListField($value, 'strtoupper');
+				return $cuisines ? ['cuisines' => $cuisines] : null;
+
+			case 'spiciness':
+				return ['spiciness' => strtoupper($value)];
+
+			default:
+				return [$fieldName => $value];
+		}
+
+		return null;
+	}
+
+	/**
+	 * Process list fields (arrays or comma-separated strings)
+	 */
+	protected function processListField($value, ?callable $transform = null): ?array
+	{
+		if (empty($value)) {
+			return null;
+		}
+
+		// Convert to array if string
+		if (is_string($value)) {
+			$value = array_map('trim', explode(',', $value));
+		}
+
+		// Filter empty values
+		$value = array_values(array_filter($value));
+
+		// Apply transformation if provided
+		if ($transform && !empty($value)) {
+			$value = array_map($transform, $value);
+		}
+
+		return !empty($value) ? $value : null;
+	}
+
+	protected function getMenuSectionsOrder(array $sections_map):array
+	{
+		$optionsMeta = Meta::forOptions('options');
+		$sectionOrder = $optionsMeta->get('menu_section_order');
+
+		// Build final GMB menu structure
+		$ordered = [];
+
+		// First add sections in configured order
+		foreach ($sectionOrder as $section) {
+			$slug = sanitize_title($section['name']);
+			if (isset($sections_map[$slug]) && !empty($sections_map[$slug]['items'])) {
+				$ordered[$slug] = $sections_map[$slug];
+				unset($sections_map[$slug]);
+			}
+		}
+
+		// Add any remaining sections
+		foreach ($sections_map as $slug => $section) {
+			if (!empty($section['items'])) {
+				$ordered[$slug] = $section;
+			}
+		}
+
+		return array_values($ordered);
+	}
+	private function getMenuCuisines(array $menu_items, array $defaults = []): array
+	{
+		$cuisines = [];
+
+		// Check if there's a default cuisine
+		if (!empty($defaults['cuisines'])) {
+			$cuisines = is_array($defaults['cuisines']) ?
+				$defaults['cuisines'] :
+				[$defaults['cuisines']];
+		}
+
+		// Collect cuisines from individual items if specified
+		foreach ($menu_items as $item) {
+			$meta = Meta::forPost($item->ID);
+			$item_cuisines = $meta->get('cuisines');
+
+			if (!empty($item_cuisines)) {
+				$item_cuisines = is_array($item_cuisines) ?
+					$item_cuisines :
+					array_map('trim', explode(',', $item_cuisines));
+
+				foreach ($item_cuisines as $cuisine) {
+					$cuisine = strtoupper(trim($cuisine));
+					if (!in_array($cuisine, $cuisines)) {
+						$cuisines[] = $cuisine;
+					}
+				}
+			}
+		}
+
+		// Ensure we have at least one cuisine (GMB requires it)
+		if (empty($cuisines)) {
+			$cuisines = ['AMERICAN'];
+		}
+
+		return array_map('strtoupper', $cuisines);
+	}
+
+	/**
+	 * Format nutrition facts for GMB API
+	 */
+		protected function processNutritionData(array $nutritionData): array
+	{
+		$processed = [];
+
+		// Handle both repeater format and simple array format
+		if (isset($nutritionData[0]) && is_array($nutritionData[0])) {
+			// Repeater format
+			foreach ($nutritionData as $fact) {
+				if (!empty($fact['name']) && isset($fact['amount'])) {
+					$processed[] = [
+						'name' => $fact['name'],
+						'amount' => floatval($fact['amount']),
+						'unit' => $fact['unit'] ?? 'GRAM'
+					];
+				}
+			}
+		} else {
+			// Simple associative array format
+			foreach ($nutritionData as $name => $value) {
+				if (!empty($value)) {
+					$unit = 'GRAM';
+					if ($name === 'calories') {
+						$unit = 'CALORIE';
+					} elseif (in_array($name, ['cholesterol', 'sodium', 'totalCarbohydrate', 'protein'])) {
+						$unit = 'MILLIGRAM';
+					}
+
+					$processed[] = [
+						'name' => $name,
+						'amount' => floatval($value),
+						'unit' => $unit
+					];
+				}
+			}
+		}
+
+		return $processed;
+	}
+
+	protected function setContentTypes():void
+	{
+		$this->contentTypes = [
+			'menu_item'	=> [
+				'nutritionFacts'	=> [
+					'type'	=> 'repeater',
+					'label'	=> 'Nutrition',
+					'fields'	=> [
+						'name'	=> [
+							'type'	=> 'select',
+							'label'	=> 'Name',
+							'options'	=> [
+								'calories' => 'Calories',
+								'totalFat'	=> 'Total Fat',
+								'cholesterol' => 'Cholesterol',
+								'sodium'	=> 'Sodium',
+								'totalCarbohydrate' => 'Total Carbohydrates',
+								'protein'	=> 'Protein'
+							]
+						],
+						'amount'	=> [
+							'type'	=> 'number',
+							'label'	=> 'Amount',
+						],
+						'unit'	=> [
+							'type' => 'select',
+							'label'	=> 'Unit',
+							'options'	=> [
+								'MILLIGRAM' => 'Milligram',
+								'GRAM'	=> 'Gram',
+								'CALORIE'	=> 'Calorie'
+							]
+						]
+					],
+					'description'	=> 'Recommended. Example: Total Fat: 3g',
+					'section'	=> 'gmb',
+				],
+				'servesNumPeople'	=> [
+					'type'	=> 'number',
+					'label'	=> 'Number of People this Serves',
+					'bulkEdit'	=> true,
+					'section'	=> 'gmb',
+				],
+				'portionSize'	=> [
+					'type'	=> 'text',
+					'label'	=> 'Portion size',
+					'description' => 'Example: 8-piece of nuggets',
+					'bulkEdit'	=> true,
+					'section'	=> 'gmb',
+				],
+				'preparationMethods' => [
+					'type'	=> 'textarea',
+					'label'	=> 'Preparation Methods',
+					'description'	=> 'Specific methods that the food can be prepared in',
+					'bulkEdit'	=> true,
+					'section'	=> 'gmb',
+				],
+				'cuisines'	=> [
+					'type'	=> 'text',
+					'label'	=> 'Cuisines',
+					'description'	=> 'Recommended. The specific cuisine of the food item',
+					'bulkEdit'	=> true,
+					'section'	=> 'gmb',
+				],
+				'spiciness'	=> [
+					'type'	=> 'select',
+					'label'	=> 'Spiciness',
+					'options'	=> [
+						'none'	=> 'None',
+						'mild'	=> 'Mild',
+						'medium'	=> 'Medium',
+						'hot'	=> 'Hot',
+					],
+					'bulkEdit'	=> true,
+					'section'	=> 'gmb',
+				],
+				'allergen'	=> [
+					'type'	=> 'checkbox',
+					'label'	=> 'Allergens',
+					'options'	=> [
+						'dairy'	=> 'Dairy',
+						'egg'	=> 'Egg',
+						'fish'	=> 'Fish',
+						'peanut'	=> 'Peanut',
+						'shellfish'	=> 'Shellfish',
+						'soy'		=> 'Soy',
+						'tree nut'	=> 'Tree Nut',
+						'wheat'		=> 'Wheat'
+					],
+					'description'	=> 'Recommended',
+					'bulkEdit'	=> true,
+					'section'	=> 'gmb',
+				],
+				'dietaryRestriction'	=> [
+					'type'	=> 'checkbox',
+					'label'	=> 'Dietary Restrictions',
+					'description' => 'Recommended.',
+					'options'	=> [
+						'halal' => 'Halal',
+						'kosher'	=> 'Kosher',
+						'organic'	=> 'Organic',
+						'vegan'		=> 'Vegan',
+						'vegetarian'	=> 'Vegetarian',
+						'gluten-free'	=> 'Gluten Free'
+					],
+					'bulkEdit'	=> true,
+					'section'	=> 'gmb',
+				]
+			],
+			'post'	=> [
+				'summary' => [
+					'type' => 'textarea',
+					'label' => 'Post Summary',
+					'required' => true
+				],
+				'language_code' => [
+					'type' => 'text',
+					'label' => 'Language',
+					'default' => 'en-CA'
+				],
+				'media' => [
+					'type' => 'image',
+					'label' => 'Photo',
+				],
+				'call_to_action' => [
+					'type' => 'group',
+					'fields' => [
+						'type' => [
+							'type' => 'select',
+							'options' =>
+								[
+									'BOOK' => 'Book Now',
+									'ORDER' => 'Order Now',
+									'SHOP' => 'Shop Now',
+									'LEARN_MORE' => 'Learn More',
+									'SIGN_UP' => 'Sign up',
+									'CALL' => 'Call'
+								]
+						],
+						'url' => [
+							'type' => 'url',
+							'label' => 'Action URL'
+						]
+					]
+				]
+			],
+			'event' => [
+				'summary' => [
+					'type' => 'textarea',
+					'label' => 'Event Description',
+					'required' => true
+				],
+				'event' => [
+					'title' => [
+						'type' => 'text',
+						'label' => 'Event Title',
+						'required' => true
+					],
+					'start_date' => [
+						'type' => 'date',
+						'label' => 'Start Date',
+						'required' => true
+					],
+					'end_date' => [
+						'type' => 'date',
+						'label' => 'End Date'
+					],
+					'start_time' => [
+						'type' => 'time',
+						'label' => 'Start Time'
+					],
+					'end_time' => [
+						'type' => 'time',
+						'label' => 'End Time'
+					]
+				],
+				'media' => [
+					'type' => 'image',
+					'label' => 'Event Photo'
+				]
+			],
+			'offer' => [
+				'summary' => [
+					'type' => 'textarea',
+					'label' => 'Offer Description',
+					'required' => true
+				],
+				'offer' => [
+					'coupon_code' => [
+						'type' => 'text',
+						'label' => 'Coupon Code'
+					],
+					'redeem_online_url' => [
+						'type' => 'url',
+						'label' => 'Redemption URL'
+					],
+					'terms_conditions' => [
+						'type' => 'textarea',
+						'label' => 'Terms & Conditions']
+				],
+				'media' => ['type' => 'image', 'label' => 'Offer Photo']
+			]
+		];
+	}
+
+
+}
diff --git a/inc/integrations/services/Helcim.php b/inc/integrations/services/Helcim.php
index 47d97fd..355e9c2 100644
--- a/inc/integrations/services/Helcim.php
+++ b/inc/integrations/services/Helcim.php
@@ -170,8 +170,6 @@
 
 	protected function setContentTypes(): void
 	{
-		$this->has_content = true;
-		$this->defaultContent = 'REGULAR';
 		$types = ['REGULAR', 'SERVICE', 'DIGITAL', 'FOOD_AND_BEV', 'EVENT', 'SUBSCRIPTION', 'DONATION'];
 
 		foreach ($types as $type) {
diff --git a/inc/integrations/services/HelcimOld.php b/inc/integrations/services/HelcimOld.php
new file mode 100644
index 0000000..22c7dff
--- /dev/null
+++ b/inc/integrations/services/HelcimOld.php
@@ -0,0 +1,1359 @@
+<?php
+
+namespace JVBase\integrations\services;
+use JVBase\integrations\Integrations;
+use JVBase\managers\queue\executors\IntegrationExecutor;
+use JVBase\managers\queue\TypeConfig;
+use JVBase\meta\Meta;
+use JVBase\ui\Checkout;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Helcim Integration Class
+ *
+ * Handles HelcimPay.js checkout, invoice retrieval, customer/card management,
+ * and bidirectional product sync via API token authentication.
+ *
+ * Helcim is the source of truth for invoices and orders.
+ * Products sync bidirectionally through the standard integration field flow.
+ *
+ * @since 1.0.0
+ */
+class HelcimOld extends Integrations
+{
+	protected static string $syncCustomer = 'helcim_sync_customer';
+	protected string $service_name = 'helcim';
+	protected string|array $apiBase = 'https://api.helcim.com/v2';
+
+	protected bool $isOAuthService = false;
+
+	/**
+	 * Helcim API rate limits
+	 * @see https://devdocs.helcim.com/docs/api-rate-limits
+	 */
+	protected array $rate_limits = [
+		'per_second' => 5,
+		'per_minute' => 60,
+		'per_hour'   => 1000
+	];
+
+	protected static array $instances = [];
+	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 self($userID);
+		}
+		return self::$instances[$key];
+	}
+
+	protected function __construct(?int $userID = null)
+	{
+		$this->title = 'Helcim';
+		$this->icon  = 'credit-card';
+
+		$this->fields = [
+			'api_token' => [
+				'type'     => 'text',
+				'subtype'  => 'password',
+				'label'    => 'API Token',
+				'hint'     => 'Found in Helcim Dashboard → Settings → API Access',
+				'required' => true,
+			],
+			'currency' => [
+				'type'    => 'select',
+				'label'   => 'Currency',
+				'options' => [
+					'CAD' => 'CAD',
+					'USD' => 'USD',
+				],
+				'default' => 'CAD',
+			],
+		];
+
+		$this->advanced = [
+			'fee_saver' => [
+				'type'  => 'true_false',
+				'label' => 'Fee Saver',
+				'hint'  => 'Pass processing fees to customers (not compatible with Google Pay)',
+			],
+			'allow_ach' => [
+				'type'  => 'true_false',
+				'label' => 'Allow ACH/Bank Payments',
+				'hint'  => 'Enable bank account payments alongside credit card',
+			],
+		];
+
+		$this->instructions = [
+			'Go to <a href="https://my.helcim.com" target="_blank">Helcim Dashboard</a>',
+			'Navigate to Settings → API Access',
+			'Create a new API Access Configuration',
+			'Enable permissions: General (Customers, Invoices, Products), Transaction Processing',
+			'Copy the API Token and paste it below',
+		];
+
+		$this->canSync = [
+			'create' => true,
+			'update' => true,
+			'delete' => false,
+		];
+
+		$this->hasWebhooks = false;
+
+		parent::__construct();
+
+		// Helcim-specific actions (processAction dispatches these)
+		$this->actions = array_merge($this->actions, [
+			'initialize_checkout' => 'initializeCheckout',
+			'get_invoices'        => 'handleGetInvoices',
+			'get_invoice'         => 'handleGetInvoice',
+			'get_customer_cards'  => 'handleGetCustomerCards',
+		]);
+
+		$this->buttons = array_merge($this->buttons, [
+			'import_from_helcim' => 'Import Products from Helcim',
+			'sync_to_helcim'    => 'Sync Site to Helcim',
+		]);
+	}
+
+	/*****************************************************************
+	 * ABSTRACT IMPLEMENTATIONS
+	 *****************************************************************/
+
+	protected function initialize(): void
+	{
+		if (empty($this->credentials)) {
+			$this->loadCredentials();
+		}
+
+		$this->apiEndpoints = [
+			'connection-test',
+			'helcim-pay/initialize',
+			'invoices',
+			'customers',
+			'payment/purchase',
+			'payment/preauth',
+			'payment/capture',
+			'payment/refund',
+			'payment/verify',
+			'card-transactions',
+			'card-batches',
+		];
+	}
+
+	protected function getRequestHeaders(): array
+	{
+		return [
+			'api-token'    => $this->credentials['api_token'] ?? '',
+			'Content-Type' => 'application/json',
+			'Accept'       => 'application/json',
+		];
+	}
+
+	protected function performConnectionTest(): bool
+	{
+		try {
+			$response = $this->getRequest('connection-test', [], null, 'none', true);
+			return !is_wp_error($response) && !$this->isErrorResponse($response ?? []);
+		} catch (Exception $e) {
+			$this->logError('Connection test failed', ['error' => $e->getMessage()]);
+			return false;
+		}
+	}
+
+	/*****************************************************************
+	 * CONTENT TYPES — product field definitions
+	 *****************************************************************/
+
+	protected function setContentTypes(): void
+	{
+		$types = ['REGULAR', 'SERVICE', 'DIGITAL', 'FOOD_AND_BEV', 'EVENT', 'SUBSCRIPTION', 'DONATION'];
+
+		foreach ($types as $type) {
+			$t = $type === 'REGULAR' ? null : $type;
+			$this->contentTypes[$type] = $this->getHelcimMeta($t);
+		}
+	}
+
+	public function getAdditionalFields(?string $content_type = null):array
+	{
+		if ($content_type === 'customer') {
+			$array = $this->getCustomerFields();
+			return array_combine(
+				array_map(fn($k) => '_square_' . $k, array_keys($array)),
+				$array
+			);
+		}
+		return array_combine(
+			array_map(fn($k) => 'hc_' . $k, array_keys($this->getHelcimMeta($content_type))),
+			$this->getHelcimMeta($content_type)
+		);
+	}
+
+	protected function getCustomerFields():array
+	{
+		return [];
+	}
+
+	/**
+	 * Get Helcim product meta fields by type.
+	 *
+	 * Used by Registrar.php  when helcim is configured
+	 */
+	public function getHelcimMeta(?string $type = null): array
+	{
+		$fields = [
+			// Basic Product Fields
+			'price' => [
+				'type'        => 'number',
+				'bulkEdit'    => true,
+				'label'       => 'Price',
+				'step'        => 0.01,
+				'max'         => 99999,
+				'description' => 'Product price'
+			],
+
+			'product_type' => [
+				'type'  => 'select',
+				'label' => 'Product Type',
+				'options' => [
+					'REGULAR'       => 'Regular Product',
+					'SERVICE'       => 'Service',
+					'DIGITAL'       => 'Digital Product',
+					'FOOD_AND_BEV'  => 'Food & Beverage',
+					'EVENT'         => 'Event/Ticket',
+					'SUBSCRIPTION'  => 'Subscription',
+					'DONATION'      => 'Donation'
+				],
+				'default' => $type ?? 'REGULAR'
+			],
+
+			'cart_quantity' => [
+				'type'   => 'number',
+				'label'  => 'Quantity',
+				'hidden' => true,
+			],
+
+			// Tax & Shipping
+			'tax_exempt' => [
+				'type'    => 'true_false',
+				'label'   => 'Tax Exempt',
+				'section' => 'helcim-tax'
+			],
+
+			'shipping_required' => [
+				'type'    => 'true_false',
+				'label'   => 'Shipping Required',
+				'section' => 'helcim-shipping'
+			],
+
+			'shipping_weight' => [
+				'type'        => 'number',
+				'label'       => 'Shipping Weight (kg)',
+				'step'        => 0.01,
+				'section'     => 'helcim-shipping',
+				'condition'   => [
+					'field'    => 'shipping_required',
+					'operator' => '==',
+					'value'    => true
+				]
+			],
+
+			// Availability
+			'available_online' => [
+				'type'    => 'true_false',
+				'label'   => 'Available Online',
+				'section' => 'helcim-availability',
+				'default' => true
+			],
+
+			'available_for_pickup' => [
+				'type'    => 'true_false',
+				'label'   => 'Available for Pickup',
+				'section' => 'helcim-availability',
+				'default' => true
+			],
+
+			'available_for_delivery' => [
+				'type'    => 'true_false',
+				'label'   => 'Available for Delivery',
+				'section' => 'helcim-availability',
+				'default' => false
+			],
+
+			'_helcim_sku' => [
+				'type'        => 'text',
+				'label'       => 'SKU',
+				'description' => 'Stock keeping unit',
+				'section'     => 'helcim-config'
+			],
+
+			// Product Variations
+			'product_variations' => [
+				'type'        => 'repeater',
+				'label'       => 'Product Variations',
+				'description' => 'Different versions of this product',
+				'add_label'   => 'Add Variation',
+				'section'     => 'variations',
+				'fields'      => $this->getHelcimVariationMeta($type)
+			],
+
+			// Product Options
+			'options' => [
+				'type'   => 'group',
+				'label'  => 'Product Options',
+				'section'=> 'helcim-options',
+				'fields' => [
+					'max_order' => [
+						'type'    => 'number',
+						'label'   => 'Maximum per order',
+						'default' => 50
+					],
+					'min_order' => [
+						'type'    => 'number',
+						'label'   => 'Minimum per order',
+						'default' => 0,
+					],
+					'step' => [
+						'type'    => 'number',
+						'label'   => 'Order increment',
+						'default' => 1,
+					],
+					'preparation_time' => [
+						'type'        => 'number',
+						'label'       => 'Preparation time (minutes)',
+						'description' => 'Time needed to prepare this item',
+						'condition'   => [
+							'field'    => 'product_type',
+							'operator' => 'in',
+							'value'    => ['FOOD_AND_BEV', 'SERVICE']
+						]
+					]
+				]
+			],
+
+			// Subscription Fields
+			'subscription_settings' => [
+				'type'      => 'group',
+				'label'     => 'Subscription Settings',
+				'section'   => 'helcim-subscription',
+				'condition' => [
+					'field'    => 'product_type',
+					'operator' => '==',
+					'value'    => 'SUBSCRIPTION'
+				],
+				'fields' => [
+					'billing_cycle' => [
+						'type'    => 'select',
+						'label'   => 'Billing Cycle',
+						'options' => [
+							'daily'     => 'Daily',
+							'weekly'    => 'Weekly',
+							'monthly'   => 'Monthly',
+							'quarterly' => 'Quarterly',
+							'yearly'    => 'Yearly'
+						],
+						'default' => 'monthly'
+					],
+					'trial_period' => [
+						'type'        => 'number',
+						'label'       => 'Trial Period (days)',
+						'description' => 'Free trial period before billing starts',
+						'default'     => 0
+					],
+					'setup_fee' => [
+						'type'        => 'number',
+						'label'       => 'Setup Fee',
+						'step'        => 0.01,
+						'description' => 'One-time setup fee'
+					]
+				]
+			],
+
+			// Food & Beverage Specific
+			'food_settings' => [
+				'type'      => 'group',
+				'label'     => 'Food & Beverage Settings',
+				'section'   => 'helcim-food',
+				'condition' => [
+					'field'    => 'product_type',
+					'operator' => '==',
+					'value'    => 'FOOD_AND_BEV'
+				],
+				'fields' => [
+					'ingredients' => [
+						'type'        => 'textarea',
+						'label'       => 'Ingredients',
+						'description' => 'List ingredients (comma separated)'
+					],
+					'allergens' => [
+						'type'        => 'checkbox_list',
+						'label'       => 'Allergens',
+						'options'     => [
+							'gluten'   => 'Contains Gluten',
+							'dairy'    => 'Contains Dairy',
+							'nuts'     => 'Contains Nuts',
+							'soy'      => 'Contains Soy',
+							'eggs'     => 'Contains Eggs',
+							'seafood'  => 'Contains Seafood'
+						]
+					],
+					'dietary_options' => [
+						'type'    => 'checkbox_list',
+						'label'   => 'Dietary Options',
+						'options' => [
+							'vegetarian'   => 'Vegetarian',
+							'vegan'        => 'Vegan',
+							'gluten_free'  => 'Gluten Free',
+							'dairy_free'   => 'Dairy Free',
+							'keto'         => 'Keto Friendly',
+							'halal'        => 'Halal',
+							'kosher'       => 'Kosher'
+						]
+					],
+					'spice_level' => [
+						'type'    => 'range',
+						'label'   => 'Spice Level',
+						'min'     => 0,
+						'max'     => 5,
+						'default' => 0
+					],
+					'serving_size' => [
+						'type'        => 'text',
+						'label'       => 'Serving Size',
+						'description' => 'e.g., "Serves 2-3"'
+					]
+				]
+			],
+
+			// Service Specific
+			'service_settings' => [
+				'type'      => 'group',
+				'label'     => 'Service Settings',
+				'section'   => 'helcim-service',
+				'condition' => [
+					'field'    => 'product_type',
+					'operator' => '==',
+					'value'    => 'SERVICE'
+				],
+				'fields' => [
+					'service_duration' => [
+						'type'        => 'number',
+						'label'       => 'Duration (minutes)',
+						'description' => 'Service duration in minutes'
+					],
+					'booking_required' => [
+						'type'  => 'true_false',
+						'label' => 'Booking Required'
+					],
+					'capacity' => [
+						'type'        => 'number',
+						'label'       => 'Service Capacity',
+						'description' => 'Maximum number of customers per service'
+					],
+					'staff_required' => [
+						'type'        => 'number',
+						'label'       => 'Staff Required',
+						'description' => 'Number of staff needed',
+						'default'     => 1
+					]
+				]
+			],
+
+			// Event Specific
+			'event_settings' => [
+				'type'      => 'group',
+				'label'     => 'Event Settings',
+				'section'   => 'helcim-event',
+				'condition' => [
+					'field'    => 'product_type',
+					'operator' => '==',
+					'value'    => 'EVENT'
+				],
+				'fields' => [
+					'event_date' => [
+						'type'  => 'datetime',
+						'label' => 'Event Date & Time'
+					],
+					'event_location' => [
+						'type'  => 'text',
+						'label' => 'Event Location'
+					],
+					'max_attendees' => [
+						'type'  => 'number',
+						'label' => 'Maximum Attendees'
+					],
+					'early_bird_price' => [
+						'type'        => 'number',
+						'label'       => 'Early Bird Price',
+						'step'        => 0.01,
+						'description' => 'Discounted price for early registrations'
+					],
+					'early_bird_deadline' => [
+						'type'  => 'date',
+						'label' => 'Early Bird Deadline'
+					]
+				]
+			]
+		];
+
+		// Add inventory fields if configured
+		if ($config['hasInventory'] ?? false) {
+			$fields['_helcim_inventory'] = [
+				'type'     => 'number',
+				'label'    => 'Inventory',
+				'bulkEdit' => true,
+				'section'  => 'inventory'
+			];
+
+			$fields['track_inventory'] = [
+				'type'    => 'true_false',
+				'label'   => 'Track Inventory',
+				'section' => 'inventory',
+				'default' => true
+			];
+
+			$fields['low_stock_threshold'] = [
+				'type'        => 'number',
+				'label'       => 'Low Stock Alert',
+				'description' => 'Alert when stock falls below this level',
+				'section'     => 'inventory',
+				'default'     => 5
+			];
+
+			$fields['product_variations']['fields']['inventory'] = [
+				'type'        => 'number',
+				'label'       => 'Stock Quantity',
+				'description' => 'Current stock for this variation'
+			];
+		}
+
+		return $fields;
+	}
+
+	public function getHelcimVariationMeta(?string $type = null):array
+	{
+
+		$base = [
+			'name' => [
+				'type'        => 'text',
+				'label'       => 'Variation Name',
+				'description' => 'e.g., "Small", "Large", "Red"'
+			],
+			'price' => [
+				'type'        => 'number',
+				'label'       => 'Price',
+				'step'        => 0.01,
+				'max'         => 99999,
+				'description' => 'Price for this variation'
+			],
+			'sku' => [
+				'type'        => 'text',
+				'label'       => 'SKU',
+				'description' => 'Stock keeping unit for this variation'
+			],
+			'track_inventory' => [
+				'type'  => 'true_false',
+				'label' => 'Track Inventory',
+			],
+			'_helcim_variation_id' => [
+				'type'        => 'text',
+				'label'       => 'Helcim Variation ID',
+				'description' => 'Helcim ID for this variation',
+				'hidden'      => true
+			],
+			'_helcim_last_sync' => [
+				'type'   => 'datetime',
+				'label'  => 'Last Sync',
+				'hidden' => true
+			],
+			'options' => [
+				'type'        => 'group',
+				'label'       => 'Variation Options',
+				'collapsible' => true,
+				'fields'      => [
+					'color' => [
+						'type'        => 'color',
+						'label'       => 'Color',
+						'description' => 'Visual color for this variation'
+					],
+					'size' => [
+						'type'    => 'select',
+						'label'   => 'Size',
+						'options' => [
+							''      => 'N/A',
+							'xs'    => 'Extra Small',
+							's'     => 'Small',
+							'm'     => 'Medium',
+							'l'     => 'Large',
+							'xl'    => 'Extra Large',
+							'xxl'   => '2X Large',
+							'custom'=> 'Custom'
+						]
+					],
+					'custom_size' => [
+						'type'        => 'text',
+						'label'       => 'Custom Size',
+						'condition'   => [
+							'field'    => 'size',
+							'operator' => '==',
+							'value'    => 'custom'
+						]
+					],
+					'weight' => [
+						'type'        => 'number',
+						'label'       => 'Weight (kg)',
+						'step'        => 0.01,
+						'description' => 'Weight of this variation'
+					],
+					'dimensions' => [
+						'type'   => 'group',
+						'label'  => 'Dimensions',
+						'fields' => [
+							'length' => [
+								'type'  => 'number',
+								'label' => 'Length (cm)',
+								'step'  => 0.1
+							],
+							'width' => [
+								'type'  => 'number',
+								'label' => 'Width (cm)',
+								'step'  => 0.1
+							],
+							'height' => [
+								'type'  => 'number',
+								'label' => 'Height (cm)',
+								'step'  => 0.1
+							]
+						]
+					]
+				]
+			]
+		];
+
+		$extras = [
+			'SERVICE' => [
+				'service_duration'	=> [
+					'type'        => 'number',
+					'label'       => 'Duration (minutes)',
+					'description' => 'Duration for this service variation'
+				],
+				'available_for_booking'	=> [
+					'type'  => 'true_false',
+					'label' => 'Available for Booking'
+				]
+			],
+
+			'FOOD_AND_BEV'	=> [
+				'portion_size'	=> [
+					'type'    => 'select',
+					'label'   => 'Portion Size',
+					'options' => [
+						'small'    => 'Small',
+						'regular'  => 'Regular',
+						'large'    => 'Large',
+						'family'   => 'Family Size'
+					]
+				],
+				'calories'	=> [
+					'type'        => 'number',
+					'label'       => 'Calories',
+					'description' => 'Calorie count for this variation'
+				]
+			],
+
+			'DIGITAL'	=> [
+				'download_limit'	=> [
+					'type'        => 'number',
+					'label'       => 'Download Limit',
+					'description' => 'Maximum number of downloads',
+					'default'     => -1
+				],
+				'expiry_days'	=> [
+					'type'        => 'number',
+					'label'       => 'Access Duration (days)',
+					'description' => 'Days until download expires',
+					'default'     => 0
+				]
+			]
+		];
+		if ($type && array_key_exists($type, $extras)){
+			$base = array_merge($base, $extras[$type]);
+		}
+		return $base;
+	}
+
+	/*****************************************************************
+	 * HELCIMPAY.JS — Frontend Scripts & Checkout Initialization
+	 *****************************************************************/
+
+	protected function registerAdditionalHooks(): void
+	{
+		if (!$this->isSetUp()) {
+			return;
+		}
+
+		add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
+
+		// Shared checkout UI (replaces provider-specific outputCheckout)
+		add_filter('jvbAdditionalActions', [Checkout::class, 'render']);
+
+		// Checkout description filter
+		add_filter('jvb_checkout_description', function (string $desc, string $provider) {
+			if ($provider === 'helcim') {
+				return 'Securely checkout with your name, email, and payments processed by Helcim.';
+			}
+			return $desc;
+		}, 10, 2);
+
+		// Register webhook endpoint (handled by parent)
+		$this->registerWebhookEndpoint();
+	}
+
+	public function enqueueScripts(): void
+	{
+		// HelcimPay.js SDK
+		wp_enqueue_script(
+			'helcim-pay-sdk',
+			'https://secure.helcim.app/helcim-pay/services/start.js',
+			[],
+			null,
+			true
+		);
+
+		// Base cart checkout (shared with Square)
+		wp_register_script(
+			'jvb-checkout',
+			JVB_URL . 'assets/js/min/checkout.min.js',
+			['jvb-utility', 'jvb-queue', 'jvb-a11y', 'jvb-cache', 'jvb-tabs', 'jvb-popup'],
+			'1.1.31',
+			true
+		);
+
+		// Helcim checkout (extends CartCheckout)
+		wp_register_script(
+			'jvb-helcim-checkout',
+			JVB_URL . 'assets/js/min/helcim.min.js',
+			['jvb-checkout', 'helcim-pay-sdk'],
+			'1.1.31',
+			true
+		);
+
+		wp_localize_script('jvb-helcim-checkout', 'helcimConfig', [
+			'api_url'      => rest_url('jvb/v1/helcim/'),
+			'nonce'        => wp_create_nonce('wp_rest'),
+			'currency'     => $this->credentials['currency'] ?? 'CAD',
+			'is_logged_in' => is_user_logged_in(),
+			'user_email'   => is_user_logged_in() ? wp_get_current_user()->user_email : '',
+			'isOpen'       => apply_filters('jvb_store_is_open', '1'),
+		]);
+
+		wp_enqueue_script('jvb-helcim-checkout');
+	}
+
+
+	protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void
+	{
+		$queue    = JVB()->queue();
+
+		$queue->registry()->register(self::$import, new TypeConfig(
+			executor:   $executor,
+			maxRetries: 3
+		));
+
+		$queue->registry()->register(self::$syncCustomer, new TypeConfig(
+			executor:   $executor,
+			maxRetries: 2
+		));
+	}
+
+	/**
+	 * Initialize a HelcimPay.js checkout session.
+	 *
+	 * Server-side: POST /helcim-pay/initialize → returns checkoutToken + secretToken.
+	 * Client-side: appendHelcimPayIframe(checkoutToken) renders the payment modal.
+	 * Tokens are valid for 60 minutes.
+	 *
+	 * @param array $data [
+	 *     'amount'      => float,   // Required
+	 *     'invoiceId'   => string,  // Optional — pay a specific invoice
+	 *     'customerId'  => int,     // Optional — associate with Helcim customer
+	 *     'paymentType' => string,  // purchase|preauth|verify (default: purchase)
+	 * ]
+	 */
+	public function initializeCheckout(array $data): array
+	{
+		if (empty($data['amount']) || (float) $data['amount'] <= 0) {
+			return ['success' => false, 'message' => 'Invalid amount'];
+		}
+
+		$paymentMethod = !empty($this->credentials['allow_ach']) ? 'cc-ach' : 'cc';
+
+		$body = [
+			'paymentType'   => $data['paymentType'] ?? 'purchase',
+			'amount'        => (float) $data['amount'],
+			'currency'      => $this->credentials['currency'] ?? 'CAD',
+			'paymentMethod' => $paymentMethod,
+		];
+
+		if (!empty($data['invoiceId'])) {
+			$body['invoiceNumber'] = $data['invoiceId'];
+		}
+
+		if (!empty($data['customerId'])) {
+			$body['customerId'] = (int) $data['customerId'];
+		}
+
+		if (!empty($this->credentials['fee_saver'])) {
+			$body['hasConvenienceFee'] = true;
+		}
+
+		$response = $this->postRequest('helcim-pay/initialize', $body);
+
+		if (is_wp_error($response)) {
+			return ['success' => false, 'message' => $response->get_error_message()];
+		}
+
+		if (empty($response['checkoutToken'])) {
+			return ['success' => false, 'message' => 'Failed to initialize checkout'];
+		}
+
+		return [
+			'success'       => true,
+			'checkoutToken' => $response['checkoutToken'],
+			'secretToken'   => $response['secretToken'] ?? '',
+		];
+	}
+
+	/*****************************************************************
+	 * INVOICES — Helcim is source of truth
+	 *****************************************************************/
+
+	/**
+	 * Get invoices for a customer.
+	 *
+	 * @param array $data ['email' => string] or ['customerId' => int]
+	 */
+	public function handleGetInvoices(array $data): array
+	{
+		$customerId = $data['customerId'] ?? null;
+
+		if (empty($customerId) && !empty($data['email'])) {
+			$customerId = $this->getCustomerIdByEmail($data['email']);
+		}
+
+		if (!$customerId) {
+			return ['success' => true, 'invoices' => []];
+		}
+
+		$response = $this->getRequest('invoices', ['customerId' => $customerId], null, 'minimal');
+
+		if (is_wp_error($response) || !is_array($response)) {
+			return ['success' => false, 'message' => 'Failed to fetch invoices'];
+		}
+
+		return ['success' => true, 'invoices' => $response];
+	}
+
+	/**
+	 * Get a single invoice by ID.
+	 */
+	public function handleGetInvoice(array $data): array
+	{
+		$invoiceId = $data['invoiceId'] ?? null;
+
+		if (!$invoiceId) {
+			return ['success' => false, 'message' => 'Invoice ID required'];
+		}
+
+		$response = $this->getRequest("invoices/{$invoiceId}", [], null, 'minimal');
+
+		if (is_wp_error($response) || !is_array($response)) {
+			return ['success' => false, 'message' => 'Failed to fetch invoice'];
+		}
+
+		return ['success' => true, 'invoice' => $response];
+	}
+
+	/*****************************************************************
+	 * CUSTOMERS & CARDS
+	 *****************************************************************/
+
+	/**
+	 * Find Helcim customer ID by email.
+	 */
+	public function getCustomerIdByEmail(string $email): ?int
+	{
+		$cacheKey = 'customer_email_' . md5($email);
+		$cached   = $this->cache->get($cacheKey);
+
+		if ($cached !== false) {
+			return (int) $cached;
+		}
+
+		$response = $this->getRequest('customers', ['search' => $email], null, 'none', true);
+
+		if (is_wp_error($response) || empty($response)) {
+			return null;
+		}
+
+		$customers = is_array($response) ? $response : [];
+		$emailLower = strtolower($email);
+
+		foreach ($customers as $customer) {
+			$contactEmail = strtolower($customer['contactEmail'] ?? $customer['email'] ?? '');
+			if ($contactEmail === $emailLower) {
+				$this->cache->set($cacheKey, $customer['id'], $this->cacheStrategy['aggressive']);
+				return (int) $customer['id'];
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Get or create a Helcim customer.
+	 */
+	public function getOrCreateCustomer(array $info): ?int
+	{
+		if (empty($info['email'])) {
+			return null;
+		}
+
+		$existing = $this->getCustomerIdByEmail($info['email']);
+		if ($existing) {
+			return $existing;
+		}
+
+		$response = $this->postRequest('customers', [
+			'contactName'  => $info['name'] ?? '',
+			'contactEmail' => $info['email'],
+			'cellphone'    => $info['phone'] ?? '',
+		]);
+
+		if (is_wp_error($response) || empty($response['id'])) {
+			return null;
+		}
+
+		return (int) $response['id'];
+	}
+
+	/**
+	 * Get saved cards for a customer.
+	 */
+	public function handleGetCustomerCards(array $data): array
+	{
+		$customerId = $data['customerId'] ?? null;
+
+		if (empty($customerId) && !empty($data['email'])) {
+			$customerId = $this->getCustomerIdByEmail($data['email']);
+		}
+
+		if (!$customerId) {
+			return ['success' => true, 'cards' => []];
+		}
+
+		$response = $this->getRequest("customers/{$customerId}/cards", [], null, 'moderate');
+
+		if (is_wp_error($response) || !is_array($response)) {
+			return ['success' => false, 'message' => 'Failed to fetch cards'];
+		}
+
+		return ['success' => true, 'cards' => $response];
+	}
+
+	/**
+	 * Get bank accounts for a customer.
+	 */
+	public function getCustomerBankAccounts(int $customerId): array
+	{
+		$response = $this->getRequest("customers/{$customerId}/bank-accounts", [], null, 'moderate');
+		return (!is_wp_error($response) && is_array($response)) ? $response : [];
+	}
+
+	/*****************************************************************
+	 * TRANSACTIONS
+	 *****************************************************************/
+
+	public function getTransactions(array $params = []): array
+	{
+		$response = $this->getRequest('card-transactions', $params, null, 'minimal');
+		return (!is_wp_error($response) && is_array($response)) ? $response : [];
+	}
+
+	public function refundPayment(array $data): array
+	{
+		$response = $this->postRequest('payment/refund', $data);
+
+		if (is_wp_error($response)) {
+			return ['success' => false, 'message' => $response->get_error_message()];
+		}
+
+		return ['success' => true, 'transaction' => $response];
+	}
+
+	/*****************************************************************
+	 * PRODUCT SYNC
+	 *****************************************************************/
+
+	protected function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void
+	{
+
+		error_log('==== [Helcim]::handleTheSavePost ====');
+		$this->queueOperation(self::$syncTo, [
+			'items'   => [$postID],
+			'user'		=> user_can($post->post_author, 'manage_options') ? null : $post->post_author
+		], [
+			'priority' => 'high',
+			'delay'    => 30,
+		]);
+		Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued');
+	}
+	/**
+	 * Handle post deletion
+	 */
+	public function handleDeletePost(int $postID): void
+	{
+		$item_id = Meta::forPost($postID)->get("_{$this->service_name}_item_id");
+
+		if (empty($item_id)) {
+			return;
+		}
+		$this->queueOperation(self::$deleteFrom, [
+			'external_ids' => [$item_id],
+			'post_id'      => $postID,
+		], [
+			'priority' => 'high',
+		]);
+
+	}
+
+	protected function handleImportFromHelcim(): array
+	{
+		$this->queueOperation('import_products', [
+			'user_id' => $this->userID,
+		], ['priority' => 'normal']);
+
+		return ['success' => true, 'message' => 'Import from Helcim queued'];
+	}
+
+	/*****************************************************************
+	 * USER ↔ CUSTOMER LINKING
+	 *****************************************************************/
+
+	public function linkUserToCustomer(int $userId, int $helcimCustomerId): void
+	{
+		update_user_meta($userId, BASE . '_helcim_customer_id', $helcimCustomerId);
+	}
+
+	public function getUserCustomerId(int $userId): ?int
+	{
+		$id = get_user_meta($userId, BASE . '_helcim_customer_id', true);
+		return $id ? (int) $id : null;
+	}
+
+	/**
+	 * Resolve customer ID from user meta, falling back to email lookup + auto-link.
+	 */
+	public function resolveCustomerId(int $userId): ?int
+	{
+		$id = $this->getUserCustomerId($userId);
+		if ($id) {
+			return $id;
+		}
+
+		$user = get_userdata($userId);
+		if (!$user || empty($user->user_email)) {
+			return null;
+		}
+
+		$id = $this->getCustomerIdByEmail($user->user_email);
+		if ($id) {
+			$this->linkUserToCustomer($userId, $id);
+		}
+
+		return $id;
+	}
+
+	/*****************************************************************
+	 * VALIDATION
+	 *****************************************************************/
+
+	/**
+	 * Validate a HelcimPay.js transaction using the secret token.
+	 *
+	 * After the frontend receives a SUCCESS message event, call this
+	 * server-side to verify the transaction hash.
+	 */
+	public function validateTransaction(string $secretToken, array $transactionData): bool
+	{
+		$hash = hash('sha256', $secretToken . json_encode($transactionData));
+		return hash_equals($hash, $transactionData['hash'] ?? '');
+	}
+	/*******************************************************************
+	 * WEBHOOKS
+	*******************************************************************/
+	/**
+	 * Validate Helcim webhook signature.
+	 *
+	 * Helcim signs webhooks with HMAC-SHA256 using:
+	 *   signedContent = "{webhook-id}.{webhook-timestamp}.{body}"
+	 *   key = base64_decode(verifierToken)
+	 *
+	 * Headers: webhook-id, webhook-timestamp, webhook-signature (v1,{base64hash})
+	 *
+	 * @see https://devdocs.helcim.com/docs/enabling-webhooks-for-transactions
+	 */
+	protected function validateWebhook(array $payload): bool
+	{
+		$headers = $payload['_headers'] ?? [];
+
+		$webhookId        = $headers['webhook_id'][0]        ?? $headers['webhook-id']        ?? '';
+		$webhookTimestamp  = $headers['webhook_timestamp'][0] ?? $headers['webhook-timestamp'] ?? '';
+		$webhookSignature = $headers['webhook_signature'][0]  ?? $headers['webhook-signature'] ?? '';
+
+		if (empty($webhookId) || empty($webhookTimestamp) || empty($webhookSignature)) {
+			$this->logError('Webhook missing required headers', [], 'warning');
+			return false;
+		}
+
+		// Verify timestamp is within 5 minutes (prevent replay attacks)
+		$now = time();
+		if (abs($now - (int)$webhookTimestamp) > 300) {
+			$this->logError('Webhook timestamp too old', [
+				'webhook_timestamp' => $webhookTimestamp,
+				'server_time'       => $now,
+			], 'warning');
+			return false;
+		}
+
+		$secret = $this->getAdvancedSetting('webhook_signature_key');
+		if (empty($secret)) {
+			// If no signature key configured, allow webhook but log warning
+			$this->logDebug('No webhook signature key configured — skipping verification');
+			return true;
+		}
+
+		// Reconstruct the raw body from the payload (minus our injected _headers)
+		$body = $payload['_raw_body'] ?? json_encode(
+			array_diff_key($payload, ['_headers' => 1, '_raw_body' => 1])
+		);
+
+		$signedContent = "{$webhookId}.{$webhookTimestamp}.{$body}";
+		$secretBytes   = base64_decode($secret);
+		$expectedHash  = base64_encode(
+			hash_hmac('sha256', $signedContent, $secretBytes, true)
+		);
+
+		// webhook-signature may contain multiple signatures: "v1,hash1 v2,hash2"
+		$signatures = explode(' ', $webhookSignature);
+		foreach ($signatures as $sig) {
+			$parts = explode(',', $sig, 2);
+			if (count($parts) === 2 && hash_equals($expectedHash, $parts[1])) {
+				return true;
+			}
+		}
+
+		$this->logError('Webhook signature mismatch', [
+			'webhook_id' => $webhookId,
+		], 'warning');
+
+		return false;
+	}
+
+	/**
+	 * Process a validated Helcim webhook event.
+	 *
+	 * Helcim sends minimal payloads: {"id": "12345", "type": "cardTransaction"}
+	 * We fetch the full transaction details from the API.
+	 */
+	protected function processWebhook(array $payload): bool
+	{
+		$type = $payload['type'] ?? '';
+		$id   = $payload['id']  ?? '';
+
+		if (empty($type) || empty($id)) {
+			$this->logError('Webhook missing type or id', $payload, 'warning');
+			return false;
+		}
+
+		return match ($type) {
+			'cardTransaction' => $this->handleTransactionWebhook($id),
+			'terminalCancel'  => $this->handleTerminalCancelWebhook($payload),
+			default           => $this->handleUnknownWebhook($type, $id),
+		};
+	}
+
+	/**
+	 * Handle a cardTransaction webhook — fetch full transaction, update records.
+	 */
+	protected function handleTransactionWebhook(string $transactionId): bool
+	{
+		// Fetch full transaction from Helcim API
+		$transaction = $this->getRequest("card-transactions/{$transactionId}");
+
+		if (is_wp_error($transaction) || empty($transaction)) {
+			$this->logError('Failed to fetch transaction for webhook', [
+				'transaction_id' => $transactionId,
+			]);
+			return false;
+		}
+
+		$status = $transaction['status'] ?? '';
+
+		// Fire action for other parts of the system to react
+		do_action('jvb_helcim_transaction', $transaction, $status);
+
+		// If linked to an invoice, update invoice cache
+		$invoiceNumber = $transaction['invoiceNumber'] ?? '';
+		if (!empty($invoiceNumber)) {
+			$this->cache->delete("invoice_{$invoiceNumber}");
+			do_action('jvb_helcim_invoice_updated', $invoiceNumber, $transaction);
+		}
+
+		// Log for debugging
+		$this->logDebug('Transaction webhook processed', [
+			'transaction_id' => $transactionId,
+			'status'         => $status,
+			'amount'         => $transaction['amount'] ?? 0,
+			'type'           => $transaction['type'] ?? '',
+		]);
+
+		return true;
+	}
+
+	/**
+	 * Handle Smart Terminal cancel webhook
+	 */
+	protected function handleTerminalCancelWebhook(array $payload): bool
+	{
+		do_action('jvb_helcim_terminal_cancel', $payload);
+
+		$this->logDebug('Terminal cancel webhook processed', [
+			'payload' => $payload,
+		]);
+
+		return true;
+	}
+
+	/**
+	 * Handle unknown webhook types (future-proofing)
+	 */
+	protected function handleUnknownWebhook(string $type, string $id): bool
+	{
+		$this->logDebug('Unknown webhook type received', [
+			'type' => $type,
+			'id'   => $id,
+		]);
+
+		do_action("jvb_helcim_webhook_{$type}", $id);
+		return true;
+	}
+
+	/**
+	 * Extract unique webhook ID for deduplication
+	 */
+	protected function extractWebhookId(array $payload): ?string
+	{
+		$headers = $payload['_headers'] ?? [];
+		return $headers['webhook_id'][0] ?? $headers['webhook-id'] ?? $payload['id'] ?? null;
+	}
+
+	/**
+	 * Override the webhook request handler to capture raw body for signature verification
+	 */
+	public function handleWebhookRequest(\WP_REST_Request $request): \WP_REST_Response
+	{
+		$payload = $request->get_params();
+		$payload['_headers']  = $request->get_headers();
+		$payload['_raw_body'] = $request->get_body();
+
+		$success = $this->handleWebhook($payload);
+
+		return new \WP_REST_Response([
+			'success' => $success,
+		], $success ? 200 : 400);
+	}
+	/***********************************************************************
+	 * POST HOOKS
+	***********************************************************************/
+	/**
+	 * Sync a WordPress post to Helcim as a product.
+	 * Called by IntegrationExecutor::processSyncTo()
+	 */
+	public function syncPostToService(int $postID): array|\WP_Error
+	{
+		$post = get_post($postID);
+		if (!$post) {
+			return new \WP_Error('not_found', "Post {$postID} not found");
+		}
+
+		$helcimProductId = get_post_meta($postID, BASE . '_helcim_item_id', true);
+		$productData     = $this->buildProductPayload($postID);
+
+		if (is_wp_error($productData)) {
+			return $productData;
+		}
+
+		if ($helcimProductId) {
+			// Update existing
+			$response = $this->patchRequest("products/{$helcimProductId}", $productData);
+		} else {
+			// Create new
+			$response = $this->postRequest('products', $productData);
+		}
+
+		if (is_wp_error($response)) {
+			update_post_meta($postID, BASE . '_helcim_sync_status', 'error');
+			return $response;
+		}
+
+		// Store Helcim product ID
+		$newId = $response['id'] ?? $response['productId'] ?? $helcimProductId;
+		update_post_meta($postID, BASE . '_helcim_item_id', $newId);
+		update_post_meta($postID, BASE . '_helcim_sync_status', 'synced');
+		update_post_meta($postID, BASE . '_helcim_last_sync', current_time('mysql'));
+
+		return ['success' => true, 'helcim_id' => $newId];
+	}
+
+	/**
+	 * Delete a product from Helcim.
+	 * Called by IntegrationExecutor::processDeleteFrom()
+	 */
+	public function deleteFromService(string $externalId): array|\WP_Error
+	{
+		$response = $this->deleteRequest("products/{$externalId}");
+
+		if (is_wp_error($response)) {
+			return $response;
+		}
+
+		return ['success' => true, 'deleted' => $externalId];
+	}
+
+	/**
+	 * Build Helcim product payload from a WordPress post.
+	 */
+	protected function buildProductPayload(int $postID): array|\WP_Error
+	{
+		$meta = \JVBase\meta\Meta::forPost($postID);
+		$post = get_post($postID);
+
+		$price = $meta->get('price');
+		if (empty($price) || !is_numeric($price)) {
+			return new \WP_Error('invalid_price', "Post {$postID} has no valid price");
+		}
+
+		return [
+			'name'        => $post->post_title,
+			'description' => wp_strip_all_tags($post->post_content),
+			'sku'         => $meta->get('sku') ?: "wp-{$postID}",
+			'price'       => (float) $price,
+			'taxExempt'   => (bool) $meta->get('tax_exempt'),
+		];
+	}
+}
diff --git a/inc/integrations/services/InstagramOld.php b/inc/integrations/services/InstagramOld.php
new file mode 100644
index 0000000..a09aa47
--- /dev/null
+++ b/inc/integrations/services/InstagramOld.php
@@ -0,0 +1,705 @@
+<?php
+/**
+ * Instagram Integration
+ * File: /inc/integrations/Instagram.php
+ */
+
+namespace JVBase\integrations\services;
+use JVBase\integrations\Integrations;
+use JVBase\meta\Meta;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class InstagramOld extends Integrations
+{
+	protected array $allowedContent = [
+		'post'
+	];
+	private string $ig_user_id = '';
+	private string $page_id = '';
+	private string $page_access_token = '';
+	private array $business_account = [];
+	private bool $auto_post = false;
+	private array $post_settings = [];
+
+	// Instagram API endpoints
+	private const API_VERSION = 'v18.0';
+	private const GRAPH_API_BASE = 'https://graph.facebook.com/';
+	private const INSTAGRAM_BASE = 'https://graph.instagram.com/';
+
+	protected static array $instances = [];
+	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 self($userID);
+		}
+
+		return self::$instances[$key];
+	}
+
+	protected function __construct(?int $userID = null)
+	{
+		$this->service_name = 'instagram';
+		$this->title = 'Instagram';
+		$this->icon = 'instagram-logo';
+		$this->apiBase = [
+			'graph' => self::GRAPH_API_BASE . self::API_VERSION,
+			'instagram' => self::INSTAGRAM_BASE . self::API_VERSION
+		];
+		$this->apiEndpoints = [
+			'me/accounts',
+			'media',
+			'media_publish',
+			'content_publishing_limit',
+			'insights'
+		];
+
+		$this->isOAuthService = true;
+		// OAuth configuration for Facebook/Instagram
+		$this->oauth = [
+			'authorize' => 'https://www.facebook.com/' . self::API_VERSION . '/dialog/oauth',
+			'token' => self::GRAPH_API_BASE . self::API_VERSION . '/oauth/access_token',
+			'redirect_uri' => rest_url('jvb/v1/oauth/instagram'),
+			'scopes' => [
+				'instagram_basic',
+				'instagram_content_publish',
+				'instagram_manage_insights',
+				'pages_show_list',
+				'pages_read_engagement',
+				'business_management'
+			]
+		];
+
+		// Define sync capabilities
+		$this->canSync = [
+			'create' => true,
+			'update' => false, // Instagram doesn't allow editing posts
+			'delete' => true
+		];
+
+		$this->fields = [
+
+		];
+
+		$this->advanced = [
+
+		];
+
+		$this->instructions = [
+			'Setup Facebook in order to use Instagram.'
+		];
+
+		$this->defaults = [
+
+		];
+		$this->hasWebhooks = true;
+		parent::__construct($userID);
+
+		$this->actions = array_merge(
+			$this->actions,
+			[
+				'switch_account'	=> 'switchInstagramAccount'
+			]
+		);
+	}
+
+	protected function initialize(): void
+	{
+		$this->page_access_token = $this->credentials['page_access_token'] ?? '';
+		$this->ig_user_id = $this->credentials['ig_user_id'] ?? '';
+		$this->page_id = $this->credentials['page_id'] ?? '';
+		$this->business_account = $this->credentials['business_account'] ?? [];
+		$this->auto_post = $this->credentials['auto_post'] ?? false;
+		$this->post_settings = $this->credentials['post_settings'] ?? [
+			'include_caption' => true,
+			'include_hashtags' => true,
+			'include_location' => false,
+			'default_hashtags' => ''
+		];
+
+		// Set rate limits
+		$this->rate_limits = [
+			'min_interval' => 30,
+			'daily_limit' => 25, // Instagram content publishing limit
+			'hourly_limit' => 5
+		];
+	}
+
+	/**
+	 * Get request headers for Instagram API
+	 */
+	protected function getRequestHeaders(): array
+	{
+		return [
+			'Content-Type' => 'application/json',
+			'Accept' => 'application/json'
+		];
+	}
+
+	/**
+	 * Build Instagram API URL
+	 */
+	protected function getApiUrl(string $endpoint, ?string $baseKey = null): string|false
+	{
+		$base = ($baseKey === 'instagram') ? $this->apiBase['instagram'] : $this->apiBase['graph'];
+
+		// Handle Instagram-specific endpoints
+		if (in_array($endpoint, ['media', 'media_publish'])) {
+			return "{$base}/{$this->ig_user_id}/{$endpoint}";
+		}
+
+		return "{$base}/{$endpoint}";
+	}
+
+	/**
+	 * Add OAuth parameters specific to Facebook/Instagram
+	 */
+	protected function addOAuthParams(array $params): array
+	{
+		$params['display'] = 'popup';
+		$params['auth_type'] = 'rerequest';
+		return $params;
+	}
+
+	public function isSetUp():bool {
+		$fb = JVB()->connect('facebook');
+		return $fb->isSetUp();
+	}
+
+	protected function getOAuthCredentials(): array
+	{
+		// Get Facebook integration
+		$facebook = JVB()->connect('facebook');
+
+		if (!$facebook || !$facebook->isSetUp()) {
+			return [];
+		}
+
+		$fbCredentials = $facebook->getCredentials();
+
+		return [
+			'client_id' => $fbCredentials['app_id'] ?? $fbCredentials['client_id'] ?? '',
+			'client_secret' => $fbCredentials['app_secret'] ?? $fbCredentials['client_secret'] ?? '',
+			'access_token' => $fbCredentials['access_token'] ?? '',
+		];
+	}
+
+	public function getCredentials():array {
+		$credentials = parent::getCredentials();
+		if (empty($credentials)) {
+			$facebook = JVB()->connect('facebook');
+
+			if (!$facebook || !$facebook->isSetUp()) {
+				return [];
+			}
+
+			$fbCredentials = $facebook->getCredentials();
+
+			return [
+				'client_id'	=> $fbCredentials['client_id'],
+				'client_secret' => $fbCredentials['client_secret'],
+			];
+		}
+		return $credentials;
+	}
+
+	private function switchInstagramAccount(array $data): WP_Error|array
+	{
+		$page_id = $data['page_id'] ?? '';
+
+		if (empty($page_id)) {
+			return [
+				'success'	=> false,
+				'message'	=> 'Page ID is required'
+			];
+		}
+
+		// Find the selected page in available pages
+		$selected_page = null;
+		foreach ($this->credentials['available_pages'] ?? [] as $page) {
+			if ($page['id'] === $page_id) {
+				$selected_page = $page;
+				break;
+			}
+		}
+
+		if (!$selected_page || empty($selected_page['instagram_business_account'])) {
+			return [
+				'success'	=> false,
+				'message'	=> 'Invalid page or no Instagram account'
+			];
+		}
+
+		// Update credentials with new page
+		$this->credentials['page_id'] = $selected_page['id'];
+		$this->credentials['page_access_token'] = $selected_page['access_token'];
+		$this->credentials['ig_user_id'] = $selected_page['instagram_business_account']['id'];
+		$this->credentials['business_account'] = $this->getBusinessProfile(
+			$selected_page['instagram_business_account']['id'],
+			$selected_page['access_token']
+		);
+
+		$this->saveCredentials($this->credentials);
+
+		// Reinitialize with new credentials
+		$this->initialize();
+
+		return [
+			'success' => true,
+			'message' => 'Switched to @' . ($this->credentials['business_account']['username'] ?? 'Instagram account'),
+			'account' => $this->credentials['business_account']
+		];
+	}
+	/**
+	 * Handle post save - queue Instagram post creation
+	 */
+	public function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void
+	{
+
+		if (!$this->hasOAuthCredentials()) {
+			error_log('OAuth Not set up for '.$this->service_name);
+			return;
+		}
+		if (!has_post_thumbnail($postID)) {
+			return;
+		}
+
+		$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');
+	}
+
+	/**
+	 * Process queued operations
+	 */
+	public function processOperation(\WP_Error|array $result, object $operation, array $data): \WP_Error|array
+	{
+		$base = strtolower($this->service_name) . '_';
+		$insta = (array_key_exists('user', $data)) ? new self((int)$data['user']) : $this;
+		switch ($operation->type) {
+			case $base . 'create_post':
+				return $insta->processCreatePost($data);
+
+			case $base . 'create_story':
+				return $insta->processCreateStory($data);
+
+			case $base . 'delete_post':
+				return $insta->processDeletePost($data);
+
+			default:
+				return $result;
+		}
+	}
+
+	private function processDeletePost(array $data): array
+	{
+		return [
+			'success' => true,
+			'message' => 'Instagram sync data removed (post remains on Instagram)',
+			'note' => 'Instagram API does not support deleting posts programmatically'
+		];
+	}
+
+	/**
+	 * Process post creation for Instagram
+	 */
+	private function processCreatePost(array $data): array
+	{
+		$post_id = $data['post_id'] ?? 0;
+		$post = get_post($post_id);
+
+		if (!$post) {
+			return ['success' => false, 'message' => 'Post not found'];
+		}
+
+		try {
+			// Get image URL
+			$image_url = get_the_post_thumbnail_url($post_id, 'full');
+			if (!$image_url) {
+				throw new Exception('No featured image found');
+			}
+
+			// Build caption
+			$caption = $this->buildCaption($post);
+
+			// Create media container
+			$container = $this->createMediaContainer($image_url, $caption);
+
+			if (!$container || empty($container['id'])) {
+				throw new Exception('Failed to create media container');
+			}
+
+			// Wait for processing (Instagram needs time to process the image)
+			sleep(10);
+
+			// Publish the media
+			$published = $this->publishMedia($container['id']);
+
+			if ($published && !empty($published['id'])) {
+				// Store Instagram media ID
+				update_post_meta($post_id, BASE . '_instagram_media_id', $published['id']);
+				update_post_meta($post_id, BASE . '_instagram_sync_status', 'synced');
+				update_post_meta($post_id, BASE . '_instagram_sync_date', current_time('mysql'));
+
+				// Get permalink if available
+				if (!empty($published['permalink'])) {
+					update_post_meta($post_id, BASE . '_instagram_permalink', $published['permalink']);
+				}
+
+				return [
+					'success' => true,
+					'message' => 'Posted to Instagram successfully',
+					'instagram_id' => $published['id']
+				];
+			}
+
+			throw new Exception('Failed to publish media');
+
+		} catch (Exception $e) {
+			$this->logError('Instagram post creation failed', [
+				'post_id' => $post_id,
+				'error' => $e->getMessage()
+			]);
+
+			update_post_meta($post_id, BASE . '_instagram_sync_status', 'failed');
+			update_post_meta($post_id, BASE . '_instagram_sync_error', $e->getMessage());
+
+			return [
+				'success' => false,
+				'message' => $e->getMessage()
+			];
+		}
+	}
+
+	/**
+	 * Build caption for Instagram post
+	 */
+	private function buildCaption(\WP_Post $post): string
+	{
+		$caption_parts = [];
+
+		// Add title and description if enabled
+		if ($this->post_settings['include_caption'] ?? true) {
+			$caption_parts[] = $post->post_title;
+
+			if (!empty($post->post_excerpt)) {
+				$caption_parts[] = "\n\n" . $post->post_excerpt;
+			}
+		}
+
+		// Add hashtags
+		if ($this->post_settings['include_hashtags'] ?? true) {
+			$hashtags = [];
+
+			// Get tags from post
+			$tags = wp_get_post_tags($post->ID, ['fields' => 'names']);
+			foreach ($tags as $tag) {
+				$hashtags[] = '#' . str_replace(' ', '', $tag);
+			}
+
+			// Add default hashtags
+			if (!empty($this->post_settings['default_hashtags'])) {
+				$default = preg_split('/[\s,]+/', $this->post_settings['default_hashtags']);
+				foreach ($default as $tag) {
+					$tag = trim($tag);
+					if (strpos($tag, '#') !== 0) {
+						$tag = '#' . $tag;
+					}
+					$hashtags[] = $tag;
+				}
+			}
+
+			if (!empty($hashtags)) {
+				$caption_parts[] = "\n\n" . implode(' ', array_unique($hashtags));
+			}
+		}
+
+		// Add website link
+		$caption_parts[] = "\n\n🔗 " . get_permalink($post);
+
+		return implode('', $caption_parts);
+	}
+
+	/**
+	 * Webhook validation
+	 */
+	protected function validateWebhook(array $payload): bool
+	{
+		// Facebook webhook verification
+		if (isset($payload['hub_mode']) && $payload['hub_mode'] === 'subscribe') {
+			$verify_token = $this->credentials['webhook_verify_token'] ?? 'jvb_instagram_webhook';
+
+			if ($payload['hub_verify_token'] === $verify_token) {
+				// Echo the challenge for verification
+				echo $payload['hub_challenge'];
+				exit;
+			}
+
+			return false;
+		}
+
+		// Validate webhook signature for actual events
+		$signature = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
+		if (!$signature) {
+			return false;
+		}
+
+		$app_secret = $this->credentials['app_secret'] ?? '';
+		if (!$app_secret) {
+			return true; // Allow if no secret configured
+		}
+
+		$expected = 'sha256=' . hash_hmac('sha256', file_get_contents('php://input'), $app_secret);
+		return hash_equals($expected, $signature);
+	}
+
+	/**
+	 * Process webhook
+	 */
+	protected function processWebhook(array $payload): bool
+	{
+		$entry = $payload['entry'][0] ?? [];
+		$changes = $entry['changes'] ?? [];
+
+		foreach ($changes as $change) {
+			$field = $change['field'] ?? '';
+			$value = $change['value'] ?? [];
+
+			switch ($field) {
+				case 'comments':
+					$this->handleCommentWebhook($value);
+					break;
+
+				case 'mentions':
+					$this->handleMentionWebhook($value);
+					break;
+
+				default:
+					$this->logDebug('Unhandled webhook field', ['field' => $field]);
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * API Helper Methods
+	 */
+
+	private function getFacebookPages(string $access_token): array
+	{
+		$response = wp_remote_get(
+			$this->apiBase['graph'] . '/me/accounts?' . http_build_query([
+				'access_token' => $access_token,
+				'fields' => 'id,name,access_token,instagram_business_account'
+			])
+		);
+
+		if (is_wp_error($response)) {
+			$this->logError('Failed to get Facebook pages', ['error' => $response->get_error_message()]);
+			return [];
+		}
+
+		$data = json_decode(wp_remote_retrieve_body($response), true);
+		return $data['data'] ?? [];
+	}
+
+	private function getBusinessProfile(string $ig_user_id, string $access_token): array
+	{
+		$response = wp_remote_get(
+			$this->apiBase['graph'] . '/' . $ig_user_id . '?' . http_build_query([
+				'access_token' => $access_token,
+				'fields' => 'id,username,name,profile_picture_url,followers_count,media_count,biography,website'
+			])
+		);
+
+		if (is_wp_error($response)) {
+			return [];
+		}
+
+		return json_decode(wp_remote_retrieve_body($response), true) ?: [];
+	}
+
+	private function createMediaContainer(string $image_url, string $caption): ?array
+	{
+		$response = wp_remote_post(
+			$this->getApiUrl('media', 'instagram'),
+			[
+				'body' => [
+					'image_url' => $image_url,
+					'caption' => $caption,
+					'access_token' => $this->page_access_token
+				]
+			]
+		);
+
+		if (is_wp_error($response)) {
+			$this->logError('Failed to create media container', ['error' => $response->get_error_message()]);
+			return null;
+		}
+
+		return json_decode(wp_remote_retrieve_body($response), true);
+	}
+
+	private function publishMedia(string $creation_id): ?array
+	{
+		$response = wp_remote_post(
+			$this->getApiUrl('media_publish', 'instagram'),
+			[
+				'body' => [
+					'creation_id' => $creation_id,
+					'access_token' => $this->page_access_token
+				]
+			]
+		);
+
+		if (is_wp_error($response)) {
+			$this->logError('Failed to publish media', ['error' => $response->get_error_message()]);
+			return null;
+		}
+
+		return json_decode(wp_remote_retrieve_body($response), true);
+	}
+
+	private function getPublishingLimit(): array
+	{
+		$response = wp_remote_get(
+			$this->getApiUrl('content_publishing_limit', 'instagram') . '?' . http_build_query([
+				'access_token' => $this->page_access_token
+			])
+		);
+
+		if (is_wp_error($response)) {
+			return new WP_Error('api_error', $response->get_error_message());
+		}
+
+		$data = json_decode(wp_remote_retrieve_body($response), true);
+
+		return [
+			'success' => true,
+			'data' => [
+				'quota_usage' => $data['data'][0]['quota_usage'] ?? 0,
+				'quota_total' => $data['data'][0]['config']['quota_total'] ?? 25,
+				'quota_duration' => $data['data'][0]['config']['quota_duration'] ?? 86400
+			]
+		];
+	}
+
+	protected function performConnectionTest(): bool
+	{
+		if (empty($this->ig_user_id) || empty($this->page_access_token)) {
+			return false;
+		}
+
+		// Test by getting account info
+		$profile = $this->getBusinessProfile($this->ig_user_id, $this->page_access_token);
+
+		if (!empty($profile['id'])) {
+			return true;
+		}
+
+		return false;
+	}
+
+	private function createStory(array $data): WP_Error|array
+	{
+		$image_url = $data['image_url'] ?? '';
+
+		if (empty($image_url)) {
+			return new WP_Error('missing_image', 'Image URL required for story');
+		}
+
+		// Queue story creation
+		$this->queueOperation('create_story', [
+			'image_url' => $image_url,
+			'sticker_url' => $data['sticker_url'] ?? ''
+		], [
+			'priority' => 'high'
+		]);
+
+		return [
+			'success' => true,
+			'message' => 'Story creation queued'
+		];
+	}
+
+	private function processCreateStory(array $data): array
+	{
+		$params = [
+			'image_url' => $data['image_url'],
+			'media_type' => 'STORIES',
+			'access_token' => $this->page_access_token
+		];
+
+		if (!empty($data['sticker_url'])) {
+			$params['sticker_url'] = $data['sticker_url'];
+		}
+
+		// Create story container
+		$response = wp_remote_post(
+			$this->getApiUrl('media', 'instagram'),
+			['body' => $params]
+		);
+
+		if (is_wp_error($response)) {
+			return [
+				'success' => false,
+				'message' => $response->get_error_message()
+			];
+		}
+
+		$container = json_decode(wp_remote_retrieve_body($response), true);
+
+		if (empty($container['id'])) {
+			return [
+				'success' => false,
+				'message' => 'Failed to create story container'
+			];
+		}
+
+		// Wait for processing
+		sleep(5);
+
+		// Publish story
+		$published = $this->publishMedia($container['id']);
+
+		return [
+			'success' => !empty($published['id']),
+			'message' => !empty($published['id']) ? 'Story published' : 'Failed to publish story',
+			'story_id' => $published['id'] ?? null
+		];
+	}
+
+	private function handleCommentWebhook(array $value): void
+	{
+		// Log new comments for moderation
+		$this->logDebug('New Instagram comment', $value);
+
+		// Could trigger notifications or store for moderation
+		do_action(BASE . 'instagram_comment_received', $value);
+	}
+
+	private function handleMentionWebhook(array $value): void
+	{
+		// Log mentions
+		$this->logDebug('New Instagram mention', $value);
+
+		// Could trigger notifications
+		do_action(BASE . 'instagram_mention_received', $value);
+	}
+
+	/**
+	 * Get service description
+	 */
+	public function getServiceDescription(): string
+	{
+		return "Connect your Instagram Business account to automatically share your content and manage your Instagram presence.";
+	}
+}
diff --git a/inc/integrations/services/PostMarkOld.php b/inc/integrations/services/PostMarkOld.php
new file mode 100644
index 0000000..9f7c7fd
--- /dev/null
+++ b/inc/integrations/services/PostMarkOld.php
@@ -0,0 +1,661 @@
+<?php
+
+namespace JVBase\integrations\services;
+use JVBase\integrations\Integrations;
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * PostMark Email Service Integration
+ *
+ * Provides integration with PostMark for transactional email delivery
+ * Hooks into WordPress's wp_mail() function for seamless integration
+ */
+class PostMarkOld extends Integrations
+{
+	protected ?string $server_token = null;
+	protected string $message_stream = 'outbound';
+	protected string $from_email;
+	protected string $from_name;
+	protected bool $track_open;
+	protected bool $track_links;
+	protected ?string $lastMessageId = null;
+
+	protected static array $instances = [];
+	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);
+		}
+		return self::$instances[$key];
+	}
+	/**
+	 * Constructor
+	 */
+	protected function __construct(?int $userID = null)
+	{
+		$this->service_name = 'postmark';
+		$this->title = 'PostMark';
+		$this->icon = 'envelope-simple';
+
+		$this->cacheName = ($userID)? 'gmb_'.$userID : 'postmark';
+
+		// PostMark API configuration
+		$this->apiBase = 'https://api.postmarkapp.com';
+		$this->apiVersion = 'v1';
+
+		// Define available endpoints
+		$this->apiEndpoints = [
+			'email',
+			'email/batch',
+			'email/withTemplate',
+			'bounces',
+			'deliverystats',
+			'messages/outbound',
+			'messages/outbound/[^/]+',
+			'messages/outbound/opens',
+			'stats/outbound',
+			'stats/outbound/opens',
+			'stats/outbound/bounces',
+			'templates',
+			'templates/[^/]+',
+			'server'
+		];
+
+		// Required credentials
+		$this->fields = [
+			'server_token' => [
+				'label' => 'Server API Token',
+				'type' => 'text',
+				'subtype'=> 'password',
+				'placeholder' => 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
+				'hint' => 'Your PostMark Server API Token'
+			],
+			'message_stream' => [
+				'label' => 'Message Stream ID',
+				'type' => 'text',
+				'placeholder' => 'outbound',
+				'default' => 'outbound',
+				'description' => 'Message stream to use (default: outbound)'
+			],
+			'from_email' => [
+				'label' => 'From Email',
+				'type' => 'email',
+				'placeholder' => 'noreply@yourdomain.com',
+				'description' => 'Default sender email address (must be verified in PostMark)'
+			],
+			'from_name' => [
+				'label' => 'From Name',
+				'type' => 'text',
+				'placeholder' => get_bloginfo('name'),
+				'default' => get_bloginfo('name'),
+				'description' => 'Default sender name'
+			],
+			'track_opens' => [
+				'label' => 'Track Opens',
+				'type' => 'select',
+				'options' => [
+					'false' => 'Disabled',
+					'true' => 'Enabled'
+				],
+				'default' => 'false',
+				'description' => 'Track when emails are opened'
+			],
+			'track_links' => [
+				'label' => 'Track Links',
+				'type' => 'select',
+				'options' => [
+					'None' => 'Disabled',
+					'HtmlOnly' => 'HTML Only',
+					'HtmlAndText' => 'HTML and Text',
+				],
+				'default' => 'None',
+				'description' => 'Track link clicks in emails'
+			]
+		];
+
+		parent::__construct();
+
+		// Hook into WordPress mail system if integration is healthy
+		if ($this->is_healthy && $this->isSetUp()) {
+			$this->initializeMailHooks();
+		}
+	}
+
+	/**
+	 * Initialize WordPress mail hooks
+	 */
+	protected function initializeMailHooks(): void
+	{
+		// Replace wp_mail with PostMark
+		add_filter('wp_mail', [$this, 'sendViaPostMark'], 10, 1);
+
+		// Optional: Hook for logging sent emails
+		add_action('postmark_email_sent', [$this, 'logSentEmail'], 10, 2);
+		add_action('postmark_email_failed', [$this, 'logFailedEmail'], 10, 2);
+	}
+
+	/**
+	 * Send email via PostMark API
+	 * Hooks into wp_mail filter
+	 *
+	 * @param array $args Email arguments from wp_mail
+	 * @return array Modified args (or original if sending via PostMark)
+	 */
+	public function sendViaPostMark(array $args): array
+	{
+		// Extract email components
+		$to = $args['to'];
+		$subject = $args['subject'];
+		$message = $args['message'];
+		$headers = $args['headers'] ?? '';
+		$attachments = $args['attachments'] ?? [];
+
+		// Parse headers
+		$parsed_headers = $this->parseMailHeaders($headers);
+
+		// Determine content type
+		$is_html = $parsed_headers['content-type'] === 'text/html' ||
+			strpos($message, '<html') !== false ||
+			strpos($message, '<body') !== false;
+
+		// Build PostMark payload
+		$payload = $this->buildEmailPayload(
+			$to,
+			$subject,
+			$message,
+			$is_html,
+			$parsed_headers,
+			$attachments
+		);
+
+		// Send via PostMark
+		$result = $this->sendEmail($payload);
+
+		if ($result === true) {
+			error_log('================================ Email sent! ================================');
+			// Prevent default wp_mail from sending
+			add_filter('pre_wp_mail', '__return_true');
+			do_action('postmark_email_sent', $args, $payload);
+		} else {
+			error_log('=-======================[POSTMARK]Something went wrong... ================================');
+			// Log failure but allow fallback to default mail
+			do_action('postmark_email_failed', $args, $result);
+
+			// Optionally fall back to default wp_mail
+			if ($this->shouldFallback()) {
+				return $args;
+			}
+		}
+
+		return $args;
+	}
+
+	/**
+	 * Build email payload for PostMark API
+	 */
+	protected function buildEmailPayload(
+		$to,
+		string $subject,
+		string $message,
+		bool $is_html,
+		array $headers,
+		array $attachments
+	): array {
+		$credentials = $this->getCredentials();
+
+		// Handle multiple recipients
+		$to_addresses = is_array($to) ? implode(',', $to) : $to;
+
+		// Build base payload
+		$payload = [
+			'From' => sprintf(
+				'%s <%s>',
+				$headers['from-name'] ?? $credentials['from_name'],
+				$headers['from'] ?? $credentials['from_email']
+			),
+			'To' => $to_addresses,
+			'Subject' => $subject,
+			'TrackOpens' => $credentials['track_opens'] === 'true',
+			'TrackLinks' => $credentials['track_links'],
+			'MessageStream' => $credentials['message_stream'] ?? 'outbound'
+		];
+
+		// Set content based on type
+		if ($is_html) {
+			$payload['HtmlBody'] = $message;
+			// Generate text version from HTML
+			$payload['TextBody'] = $this->generateTextFromHtml($message);
+		} else {
+			$payload['TextBody'] = $message;
+		}
+
+		// Add CC/BCC if present
+		if (!empty($headers['cc'])) {
+			$payload['Cc'] = is_array($headers['cc']) ?
+				implode(',', $headers['cc']) : $headers['cc'];
+		}
+
+		if (!empty($headers['bcc'])) {
+			$payload['Bcc'] = is_array($headers['bcc']) ?
+				implode(',', $headers['bcc']) : $headers['bcc'];
+		}
+
+		// Add Reply-To if present
+		if (!empty($headers['reply-to'])) {
+			$payload['ReplyTo'] = $headers['reply-to'];
+		}
+
+		// Handle attachments
+		if (!empty($attachments)) {
+			$payload['Attachments'] = $this->processAttachments($attachments);
+		}
+
+		// Add custom headers if needed
+		if (!empty($headers['x-headers'])) {
+			$payload['Headers'] = $headers['x-headers'];
+		}
+
+		// Add tags for tracking (optional)
+		$payload['Tag'] = $this->determineEmailTag($subject, $headers);
+
+		return $payload;
+	}
+
+	/**
+	 * Send email via PostMark API
+	 */
+	protected function sendEmail(array $payload): bool|WP_Error
+	{
+		if (!$this->isSetUp()) {
+			return false;
+		}
+		try {
+			$response = $this->postRequest('email', $payload);
+			error_log('================================ POSTMARK RESPONSE: ================================'.print_r($response, true));
+			if (is_wp_error($response)) {
+				return $response;
+			}
+
+			// Store message ID for tracking
+			if (!empty($response['MessageID'])) {
+				$this->lastMessageId = $response['MessageID'];
+
+				// Cache for potential status checking
+				$this->cache->set(
+					'postmark_message_' . $response['MessageID'],
+					$payload,
+					3600 // 1 hour
+				);
+			}
+
+			return true;
+
+		} catch (\Exception $e) {
+			$this->logError('[POSTMARK]', ['method' => 'sendEmail', 'error' => $e->getMessage()]);
+			return new WP_Error(
+				'postmark_send_failed',
+				$e->getMessage()
+			);
+		}
+	}
+
+	/**
+	 * Parse mail headers from various formats
+	 */
+	protected function parseMailHeaders($headers): array
+	{
+		$parsed = [
+			'content-type' => 'text/plain',
+			'from' => null,
+			'from-name' => null,
+			'reply-to' => null,
+			'cc' => [],
+			'bcc' => [],
+			'x-headers' => []
+		];
+
+		if (empty($headers)) {
+			return $parsed;
+		}
+
+		// Handle array format
+		if (is_array($headers)) {
+			foreach ($headers as $header) {
+				$this->parseHeaderLine($header, $parsed);
+			}
+		} else {
+			// Handle string format (multiple lines)
+			$lines = explode("\n", $headers);
+			foreach ($lines as $line) {
+				$this->parseHeaderLine($line, $parsed);
+			}
+		}
+
+		return $parsed;
+	}
+
+	/**
+	 * Parse individual header line
+	 */
+	protected function parseHeaderLine(string $line, array &$parsed): void
+	{
+		if (empty($line)) {
+			return;
+		}
+
+		// Split header name and value
+		$parts = explode(':', $line, 2);
+		if (count($parts) !== 2) {
+			return;
+		}
+
+		$name = strtolower(trim($parts[0]));
+		$value = trim($parts[1]);
+
+		switch ($name) {
+			case 'content-type':
+				if (strpos($value, 'text/html') !== false) {
+					$parsed['content-type'] = 'text/html';
+				}
+				break;
+
+			case 'from':
+				// Parse "Name <email>" format
+				if (preg_match('/^(.+?)\s*<(.+)>$/', $value, $matches)) {
+					$parsed['from-name'] = trim($matches[1], '"\'');
+					$parsed['from'] = $matches[2];
+				} else {
+					$parsed['from'] = $value;
+				}
+				break;
+
+			case 'reply-to':
+				$parsed['reply-to'] = $value;
+				break;
+
+			case 'cc':
+				$parsed['cc'][] = $value;
+				break;
+
+			case 'bcc':
+				$parsed['bcc'][] = $value;
+				break;
+
+			default:
+				// Store custom headers
+				if (strpos($name, 'x-') === 0) {
+					$parsed['x-headers'][] = [
+						'Name' => $parts[0], // Original case
+						'Value' => $value
+					];
+				}
+		}
+	}
+
+	/**
+	 * Process attachments for PostMark
+	 */
+	protected function processAttachments(array $attachments): array
+	{
+		$processed = [];
+
+		foreach ($attachments as $file) {
+			if (!file_exists($file)) {
+				continue;
+			}
+
+			$processed[] = [
+				'Name' => basename($file),
+				'Content' => base64_encode(file_get_contents($file)),
+				'ContentType' => mime_content_type($file) ?: 'application/octet-stream'
+			];
+		}
+
+		return $processed;
+	}
+
+	/**
+	 * Generate plain text from HTML content
+	 */
+	protected function generateTextFromHtml(string $html): string
+	{
+		// Remove HTML tags
+		$text = strip_tags($html);
+
+		// Convert entities
+		$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+
+		// Clean up whitespace
+		$text = preg_replace('/\s+/', ' ', $text);
+		return trim($text);
+	}
+
+	/**
+	 * Determine email tag based on content
+	 */
+	protected function determineEmailTag(string $subject, array $headers): string
+	{
+		// Password reset emails
+		if (stripos($subject, 'password') !== false && stripos($subject, 'reset') !== false) {
+			return 'password-reset';
+		}
+
+		// New user registration
+		if (stripos($subject, 'welcome') !== false || stripos($subject, 'registration') !== false) {
+			return 'registration';
+		}
+
+		// Account notifications
+		if (stripos($subject, 'account') !== false) {
+			return 'account';
+		}
+
+		// Order/transaction emails
+		if (stripos($subject, 'order') !== false || stripos($subject, 'receipt') !== false) {
+			return 'transaction';
+		}
+
+		// Default
+		return 'general';
+	}
+
+	/**
+	 * Check if should fallback to default mail
+	 */
+	protected function shouldFallback(): bool
+	{
+		// Don't fallback if we've had too many errors
+		if ($this->error_stats['consecutive_errors'] >= 3) {
+			return false;
+		}
+
+		// Check if fallback is enabled in settings
+		$credentials = $this->getCredentials();
+		return ($credentials['enable_fallback'] ?? 'true') === 'true';
+	}
+
+	/**
+	 * Override makeRequest to add PostMark headers
+	 */
+	protected function getRequestHeaders(): array {
+		$credentials = $this->getCredentials();
+
+		// Add PostMark specific headers
+		return [
+			'Accept' => 'application/json',
+			'Content-Type' => 'application/json',
+			'X-Postmark-Server-Token' => $credentials['server_token'] ?? ''
+		];
+	}
+
+	/**
+	 * Get email statistics
+	 */
+	public function getEmailStats(int $days = 30): array
+	{
+		$cache_key = 'postmark_stats_' . $days;
+		$cached = $this->cache->get($cache_key);
+
+		if ($cached !== false) {
+			return $cached;
+		}
+
+		try {
+			$stats = $this->getRequest('stats/outbound', [
+				'fromdate' => date('Y-m-d', strtotime("-{$days} days")),
+				'todate' => date('Y-m-d')
+			]);
+
+			if (!is_wp_error($stats)) {
+				$this->cache->set($cache_key, $stats, 3600); // Cache for 1 hour
+				return $stats;
+			}
+		} catch (\Exception $e) {
+			$this->logError('stats/outbound', ['error' => $e->getMessage()]);
+		}
+
+		return [];
+	}
+
+	/**
+	 * Get bounce information
+	 */
+	public function getBounces(int $count = 25, int $offset = 0): array
+	{
+		try {
+			return $this->getRequest('bounces', [
+				'count' => $count,
+				'offset' => $offset
+			]);
+		} catch (\Exception $e) {
+			$this->logError('[POSTMARK]', ['method' => 'getBounces', 'error' => $e->getMessage()]);
+			return [];
+		}
+	}
+
+	/**
+	 * Log sent emails for debugging/tracking
+	 */
+	public function logSentEmail(array $args, array $payload): void
+	{
+		if (!defined('WP_DEBUG') || !WP_DEBUG) {
+			return;
+		}
+
+		$this->logDebug('Email sent via PostMark', [
+			'to' => $args['to'],
+			'subject' => $args['subject'],
+			'message_id' => $this->lastMessageId ?? 'unknown',
+			'tag' => $payload['Tag'] ?? 'none'
+		]);
+	}
+
+	/**
+	 * Log failed email attempts
+	 */
+	public function logFailedEmail(array $args, $error): void
+	{
+		$error_message = is_wp_error($error) ? $error->get_error_message() : 'Unknown error';
+
+		$this->logError('Failed to send email via PostMark', [
+			'to' => $args['to'],
+			'subject' => $args['subject'],
+			'error' => $error_message
+		]);
+	}
+
+	/**
+	 * Test email configuration
+	 */
+	public function sendTestEmail(string $to_email): bool|WP_Error
+	{
+		$payload = [
+			'From' => $this->getCredentials()['from_email'] . ' <' . $this->getCredentials()['from_email'] . '>',
+			'To' => $to_email,
+			'Subject' => 'PostMark Test Email - ' . get_bloginfo('name'),
+			'TextBody' => 'This is a test email from your PostMark integration.',
+			'HtmlBody' => '<p>This is a <strong>test email</strong> from your PostMark integration.</p>',
+			'Tag' => 'test',
+			'TrackOpens' => false,
+			'TrackLinks' => 'None'
+		];
+
+		return $this->sendEmail($payload);
+	}
+
+	/**
+	 * Render additional options in dashboard
+	 */
+	public function renderAdditionalOptions(): void
+	{
+		$stats = $this->getEmailStats(7);
+
+		?>
+		<div class="postmark-dashboard">
+			<h3>Email Statistics (Last 7 Days)</h3>
+
+			<?php if (!empty($stats)): ?>
+				<div class="stats-grid">
+					<div class="stat-item">
+						<span class="stat-value"><?= number_format($stats['Sent'] ?? 0) ?></span>
+						<span class="stat-label">Emails Sent</span>
+					</div>
+					<div class="stat-item">
+						<span class="stat-value"><?= number_format($stats['Bounced'] ?? 0) ?></span>
+						<span class="stat-label">Bounced</span>
+					</div>
+					<div class="stat-item">
+						<span class="stat-value"><?= $stats['Opens'] ?? 0 ?>%</span>
+						<span class="stat-label">Open Rate</span>
+					</div>
+				</div>
+			<?php else: ?>
+				<p>No statistics available. Check your API configuration.</p>
+			<?php endif; ?>
+
+			<div class="test-email-section">
+				<h4>Send Test Email</h4>
+				<form id="postmark-test-email" method="post">
+					<input type="email"
+						   name="test_email"
+						   placeholder="test@example.com"
+						   value="<?= esc_attr(wp_get_current_user()->user_email) ?>"
+						   required>
+					<button type="submit" class="button">Send Test</button>
+				</form>
+			</div>
+
+			<?php if ($bounces = $this->getBounces(5)): ?>
+				<div class="recent-bounces">
+					<h4>Recent Bounces</h4>
+					<ul>
+						<?php foreach ($bounces as $bounce): ?>
+							<li>
+								<?= esc_html($bounce['Email']) ?> -
+								<?= esc_html($bounce['Type']) ?>
+								<small>(<?= esc_html($bounce['BouncedAt']) ?>)</small>
+							</li>
+						<?php endforeach; ?>
+					</ul>
+				</div>
+			<?php endif; ?>
+		</div>
+		<?php
+	}
+
+	protected function initialize(): void
+	{
+		if (empty($this->credentials)) {
+			$this->loadCredentials();
+		}
+		$this->server_token = (array_key_exists('server_token', $this->credentials)) ? $this->credentials['server_token'] : null;
+		$this->from_email = (array_key_exists('from_email', $this->credentials)) ? $this->credentials['from_email'] : get_bloginfo('admin_email');
+		$this->from_name = (array_key_exists('from_name', $this->credentials)) ? $this->credentials['from_name'] : get_bloginfo('name');
+		$this->message_stream = (array_key_exists('message_stream', $this->credentials)) ? $this->credentials['message_stream'] : 'outbound';
+		$this->track_open = (array_key_exists('track_open', $this->credentials)) ? $this->credentials['track_open'] : false;
+		$this->track_links = (array_key_exists('track_links', $this->credentials)) ? $this->credentials['track_links'] : false;
+	}
+}
diff --git a/inc/integrations/services/Square.php b/inc/integrations/services/Square.php
index 0d18f51..e3dbfff 100644
--- a/inc/integrations/services/Square.php
+++ b/inc/integrations/services/Square.php
@@ -2,177 +2,93 @@
 
 namespace JVBase\integrations\services;
 use Exception;
+use JVBase\integrations\Admin;
+use JVBase\integrations\Instance;
 use JVBase\integrations\Integrations;
+use JVBase\integrations\OAuth;
+use JVBase\integrations\OAuthCredentials;
+use JVBase\integrations\OAuthURLs;
+use JVBase\integrations\Requests;
+use JVBase\integrations\SyncDefaults;
+use JVBase\integrations\SyncFrom;
+use JVBase\integrations\SyncTo;
+use JVBase\integrations\Webhooks;
 use JVBase\managers\queue\executors\IntegrationExecutor;
 use JVBase\managers\queue\TypeConfig;
-use JVBase\meta\Form;
-use JVBase\meta\Meta;
+use JVBase\meta\Sanitizer;
 use JVBase\registrar\Registrar;
-use JVBase\ui\Checkout;
-use JVBase\ui\CRUDSkeleton;
 use WP_Error;
-use WP_Post;
-use WP_Query;
-use WP_User;
 
 if (!defined('ABSPATH')) {
 	exit;
 }
 
-/**
- * Square Integration Class
- *
- * Handles OAuth 2.0 authentication and API interactions with Square
- * Uses the latest Square API version with OAuth code flow
- *
- * @since 1.0.0
- */
 class Square extends Integrations
 {
-	protected static string $syncCustomer = 'square_sync_customer';
-	protected array $allowedContent = [
-		'REGULAR',
-		'FOOD_AND_BEV',
-		'APPOINTMENTS_SERVICE',
-		'DIGITAL',
-		'EVENT',
-		'DONATION'
-	];
-	/**
-	 * Square API Configuration
-	 */
-	protected string $service_name = 'square';
-	protected array|string $apiBase = [
-		'production' 	=> 'https://connect.squareup.com',
-		'sandbox'		=> 'https://connect.squareupsandbox.com'
-	];
-
-	protected string $apiVersion = '2025-09-24';
-
-	protected string $environment = 'sandbox';
-
-	private const PASSWORD_RESET_INTERVAL = 3; // Reset password every 3 logins, for customers
-
-	/**
-	 * OAuth Configuration
-	 */
-	protected bool $isOAuthService = true;
+	use Admin,
+		Instance,
+		Requests,
+		OAuth,
+		SyncFrom,
+		SyncTo,
+		SyncDefaults,
+		Webhooks;
 
 	protected string $orderPostType = '_square_order';
-	protected array $newOrder = [];
-	protected array $oauth = [
-		'authorize' => '',
-		'token' => '',
-		'revoke' => '',
-		'scopes' => [
-			'MERCHANT_PROFILE_READ',
-			'MERCHANT_PROFILE_WRITE',
-			'PAYMENTS_WRITE',
-			'PAYMENTS_READ',
-			'CUSTOMERS_WRITE',
-			'CUSTOMERS_READ',
-			'INVENTORY_READ',
-			'INVENTORY_WRITE',
-			'ITEMS_READ',
-			'ITEMS_WRITE',
-			'ORDERS_READ',
-			'ORDERS_WRITE'
-		]
-	];
+	protected string $apiVersion = '2025-09-24';
 
-	/**
-	 * Square-specific properties
-	 */
-	protected string $merchantId = '';
-	protected string $locationId = '';
-	protected array $locations = [];
-
-	protected static Square $instance;
-
-	public static function getInstance():self
+	protected function __construct()
 	{
-		if (!isset(self::$instance)) {
-			self::$instance = new self();
-		}
-		return self::$instance;
-	}
-
-	protected function __construct(?int $userID = null)
-	{
-
-		// Display properties
 		$this->title = 'Square';
 		$this->icon = 'square-logo';
+		$this->supportsWebp = false;
 
-		$this->refresh_interval = 7 * DAY_IN_SECONDS;
-
-		$this->newOrder = [
-			'post_type'		=> $this->orderPostType,
-			'post_status' 	=> 'PROPOSED',
+		$this->canSync = [
+			'create'	=> true,
+			'update'	=> true,
+			'delete'	=> true,
 		];
 
-		// Define credential fields
-		$this->fields = [
-			'environment'	=> [
-				'type'	=> 'select',
-				'label'	=> 'Environment',
-				'options'	=> [
-					'sandbox'	=> 'Sandbox',
-					'production'=> 'Production',
-				],
-				'default'	=> 'sandbox',
-			],
-			'client_id' => [
-				'type' => 'text',
-				'label' => 'Application ID',
-				'hint' => 'Found in Square Developer Dashboard',
-				'required' => true,
-			],
-			'client_secret' => [
-				'type' => 'text',
-				'subtype' => 'password',
-				'label' => 'Application Secret',
-				'hint' => 'Found in OAuth section of Square Developer Dashboard',
-				'required' => true,
-			],
-			'location_id' => [
-				'type' => 'select',
-				'label' => 'Default Location',
-				'options' => [],
-				'hint' => 'Select default Square location for transactions',
-			]
-		];
+		$this->defineFields();
+		$this->defineInstructions();
+		$this->defineOAuth();
 
-		// Advanced settings
-		$this->advanced = [
-			'webhook_signature_key' => [
-				'type' => 'text',
-				'subtype' => 'password',
-				'label' => 'Webhook Signature Key',
-				'hint' => 'Used to verify webhook authenticity',
-			],
-			'access_token' => [
-				'type' => 'text',
-				'subtype' => 'password',
-				'label' => 'Access Token',
-				'hint' => 'Generated automatically after OAuth authorization',
-				'readonly' => true,
-			],
-			'refresh_token' => [
-				'type' => 'text',
-				'subtype' => 'password',
-				'label' => 'Refresh Token',
-				'hint' => 'Used to refresh expired access tokens',
-				'readonly' => true,
-			],
-			'merchant_id' => [
-				'type' => 'text',
-				'label' => 'Merchant ID',
-				'hint' => 'Square Merchant ID (obtained after authorization)',
-				'readonly' => true,
-			],
-		];
+		$this->baseUrl = $this->getBaseURL();
 
+		parent::__construct();
+
+		$this->defineActions();
+	}
+
+	protected function defineFields():void
+	{
+		$env = $this->addField('environment', 'Environment','select','manage_options');
+			$env->setOptions([
+				'sandbox'	=> 'Sandbox',
+				'production'=> 'Production'
+			]);
+			$env->setDefault('sandbox');
+		$id = $this->addField('client_id', 'Application ID', 'text', 'manage_options');
+			$id->setRequired();
+			$id->setHint('Found in Square Developer Dashboard');
+
+		$sec = $this->addField('client_secret', 'Application Secret', 'text', 'manage_options');
+			$sec->setSubType('password');
+			$sec->setRequired();
+			$sec->setHint('Found in OAuth section of Square Developer Dashboard');
+
+		//Locations are set from the first response from the integration
+		$locs = $this->addField('locations', 'Locations', 'select','manage_options');
+			$locs->setHidden();
+			$locs->setOptions($this->getLocationOptions());
+
+		$loc = $this->addField('location_id', 'Default Location', 'select', 'manage_options');
+			$loc->setOptions($this->loadCredentials()['locations']);
+			$loc->setHint('Select default Square location for transactions');
+	}
+
+	protected function defineInstructions():void
+	{
 		$this->instructions = [
 			'Go to <a href="https://developer.squareup.com/apps" target="_blank">Square Developer Dashboard</a>',
 			'Create a new application or select an existing one',
@@ -181,79 +97,329 @@
 			'Copy the Application ID and Application Secret',
 			'Click "Authorize Connection" below to complete OAuth flow'
 		];
+	}
 
-
-		$this->canSync = [
-			'create' => true,
-			'update' => true,
-			'delete' => true
-		];
-		$this->supportsWebp = false;
-		$this->hasWebhooks = true;
-
-		parent::__construct();
-
-		// Add Square-specific actions
-		$this->actions = array_merge(
-			$this->actions,
-			[
-				'select_location' => 'handleSelectLocation',
-				'fetch_locations' => 'handleFetchLocations',
-				'verify_webhook' => 'handleVerifyWebhook',
-				'test_payment' => 'handleTestPayment'
-			]
+	protected function defineActions():void
+	{
+		$this->addAction(
+			'Select Location',
+			[$this, 'handleSelectLocation'],
+			'select_location',
+			'building-office',
 		);
 
-		$this->buttons = array_merge(
-			$this->buttons,
-			[
-				'import_from_square'	=> 'Import Catalog from Square',
-				'sync_to_square' => 'Sync Site to Square',
-			]
+		$this->addAction(
+			'Fetch Locations',
+			[$this, 'getLocations'],
+			'fetch_locations',
+			'building-office',
 		);
 
+		$this->addAction(
+			'Import From Square',
+			[$this, 'handleImportFromSquare'],
+			'import_from_square',
+			'download-simple'
+		);
+
+		$this->addAction(
+			'Sync to Square',
+			[$this, 'handleSyncToSquare'],
+			'sync_to_square',
+			'export',
+		);
+
+//		$this->addAction(
+//			'Verify Webhook',
+//			[$this, 'handleVerifyWebhook'],
+//			'verify_webhook',
+//		);
+	}
+
+	protected function defineOAuth():void
+	{
+		$this->baseOAuthUrl = $this->getBaseURL();
+		$base = $this->getBaseURL();
+
+		$this->oauth = new OAuthURLs(
+			sprintf('%s/oauth2/authorize', $base),
+			sprintf('%s/oauth2/token', $base),
+			sprintf('%s/oauth2/revoke', $base)
+		);
+	}
+
+	protected function registerAdditionalHooks():void
+	{
 		add_action('init', [$this, 'registerSquarePostTypes'], 5);
 		add_action('init', [$this, 'addDashboardPages'], 10);
 		add_action(BASE.'dashboard_page_orders', [$this, 'renderDashPage']);
 	}
 
-	/**
-	 * Initialize the integration with loaded credentials
-	 */
 	protected function initialize(): void
 	{
-		if (empty($this->credentials)) {
-			$this->loadCredentials();
-		}
-
-		$this->merchantId = $this->credentials['merchant_id'] ?? '';
-		$this->locationId = $this->credentials['location_id'] ?? '';
-		$this->environment = $this->credentials['environment'] ?? 'sandbox';
-
-		// Set OAuth URLs based on environment
-		$baseUrl = ($this->environment === 'production')
-			? 'https://connect.squareup.com'
-			: 'https://connect.squareupsandbox.com';
-
-		$this->oauth['authorize'] = $baseUrl . '/oauth2/authorize';
-		$this->oauth['token'] = $baseUrl . '/oauth2/token';
-		$this->oauth['revoke'] = $baseUrl . '/oauth2/revoke';
-
-		// Set up API endpoints with version
-		$this->apiEndpoints = [
-			'locations' => '/v2/locations',
-			'customers' => '/v2/customers',
-			'catalog' => '/v2/catalog',
-			'payments' => '/v2/payments',
-			'orders' => '/v2/orders',
-			'inventory' => '/v2/inventory'
-		];
-
+		// TODO: Implement initialize() method.
 	}
 
-	public function getOrderFields():array
+	protected function refreshAccessToken(OAuthCredentials $credentials): array
+	{
+		if (!$this->getClientID() || !$this->getClientSecret())
+		$response = wp_remote_post(
+			$this->oauth->getToken(),
+			[
+				'body'	=> [
+					'client_id'		=> $this->getClientID(),
+					'client_secret' => $this->getClientSecret(),
+					'refresh_token'	=> $credentials->refresh_token,
+					'grant_type'	=> 'refresh_token'
+				],
+				'headers'	=> $this->getOAuthRequestHeaders()
+			]
+		);
+		if (is_wp_error($response)) {
+			$this->logError(
+				'refreshAccessToken',
+			'Failed to refresh Square token',
+				[
+					'error'	=> $response->get_error_message()
+				]
+			);
+			return $this->response(false, 'Could not refresh token');
+		}
+
+		$data = json_decode(wp_remote_retrieve_body($response));
+		if (isset($data['access_token'])) {
+			$credentials = [
+				'access_token'	=>  $data['access_token'],
+				'expires_at'	=> time() + ($data['expires_in'] ?? 2592000) // 30 days default
+			];
+
+			$this->saveCredentials($credentials);
+			return $this->response(true, 'Successfully refreshed access token');
+		}
+		return $this->response(false, 'Something went wrong that wasn\'t caught - could not refresh access token');
+	}
+
+	protected function getRequestHeaders(): array
 	{
 		return [
+			'Authorization' => 'Bearer ' . ($this->getAccessToken()),
+			'Square-Version' => $this->apiVersion,
+			'Content-Type' => 'application/json'
+		];
+	}
+
+	protected function handleResponse(\WP_Error|array $response, string $method, string $endpoint): array
+	{
+		if (is_wp_error($response) && $response->get_error_message() === 'rate_limit_exceeded') {
+			return $this->response(false, 'Rate limit exceeded');
+		}
+		$endpoint = str_replace($this->getBaseURL(), '', $endpoint);
+		switch ($endpoint) {
+			case '/v2/customers/search':
+				return $this->handleEmailSearchResponse($response, $method);
+			case '/v2/customers':
+				return $this->handleCreateUserResponse($response, $method);
+			case '/v2/locations':
+				return $this->handleGetLocationResponse($response, $method);
+		}
+		return $this->response(false, 'Unhandled Response for '.$endpoint.' endpoint, with method: '.$method);
+	}
+
+	protected function setContentTypes(): void
+	{
+		// TODO: Implement setContentTypes() method.
+	}
+
+	protected function formatForService(int $itemID, string $type = 'post'): array
+	{
+		return match ($type) {
+			'post'	=> $this->formatPostItem($itemID),
+			'user'	=> $this->formatUserItem($itemID),
+			'term'	=> $this->formatTermItem($itemID),
+			default => []
+		};
+	}
+
+	public function handleEmailSearch(string $email): string|false
+	{
+		try {
+			$response = $this->postRequest(
+				sprintf('%s/v2/customers/search', $this->getBaseURL()),
+				[
+					'query'	=> [
+						'filter'	=> [
+							'email_address'	=> [
+								'exact'	=> $email
+							]
+						]
+					]
+				]
+			);
+			return $response['id'];
+		} catch (Exception $e) {
+			return false;
+		}
+	}
+		protected function handleEmailSearchResponse(WP_Error|array $response, string $method):array
+		{
+
+		}
+
+	protected function handleCreateUser(array $data): array
+	{
+		try {
+			return $this->postRequest(
+				sprintf('%s/v2/customers',$this->getBaseURL()),
+				$data
+			);
+		} catch (Exception $e) {
+			return $this->response(false, 'Failed to create customer: '.$e->getMessage());
+		}
+	}
+		protected function handleCreateUserResponse(WP_Error|array $response, string $method):array
+		{
+
+		}
+	protected function validateUserFields(int $userID, array $fields):array|false
+	{
+		$allowed = [
+			'given_name',
+			'family_name',
+			'company_name',
+			'email_address',
+			'phone_number',
+			'idempotency_key',
+			'nickname',
+			'address',
+			'reference_id', // The WP user id, in this case
+			'note',
+			'birthday'
+		];
+		$fields = array_filter($fields, function ($f) use ($allowed, $fields) {
+			if (!in_array($f, $allowed)) {
+				$this->logError('validateUserFields', 'Stripped attempted field for user: '.$f.' with value: '.print_r($fields[$f], true));
+			}
+			return in_array($f, $allowed) && !empty($fields[$f]);
+		}, ARRAY_FILTER_USE_KEY);
+		if (empty($fields)) {
+			$this->logError('validateUserFields', 'No valid fields found');
+			return false;
+		}
+
+
+		//Square requires at least one of these fields
+		$any = [
+			'given_name',
+			'family_name',
+			'company_name',
+			'email_address',
+			'phone_number'
+		];
+
+		$doIt = false;
+		foreach ($any as $a) {
+			if (array_key_exists($a, $fields) && !empty($fields[$a])) {
+				$doIt = true;
+			}
+		}
+		if (!$doIt) {
+			$this->logError('validateUserFields', 'We need at least one of the following fields: '.print_r($any, true));
+			return false;
+		}
+
+		$addressAllowed = [
+			'address_line_1',
+			'address_line_2',
+			'address_line_3',
+			'locality',			// city
+			'sublocality',		//
+			'sublocality_2',	//
+			'sublocality_3',	//
+			'administrative_district_1',	//Province, in our case
+			'administrative_district_2',	//
+			'administrative_district_3',	//
+			'postal_code',
+			'country',
+			'first_name',
+			'last_name',
+		];
+		if (array_key_exists('address', $fields)) {
+			$fields['address'] = array_filter($fields['address'], function ($f) use ($addressAllowed, $fields){
+				return array_key_exists($f, $addressAllowed) && !empty($fields['address'][$f]);
+			}, ARRAY_FILTER_USE_KEY);
+		}
+
+		foreach ($fields as $f => $v) {
+			$v = match ($f) {
+				'email' => filter_var($v, FILTER_SANITIZE_EMAIL),
+				'phone' => Sanitizer::sanitizePhone($v),
+				default => sanitize_text_field($v),
+			};
+			if (empty($v) && in_array($f, $this->requiredUserFields())) {
+				return false;
+			}
+			$fields[$f] = $v;
+		}
+		//Add the WP User ID as the reference
+		$fields['reference_id'] = $userID;
+		return $fields;
+	}
+
+
+	protected function validateWebhook(array $payload): bool
+	{
+		// TODO: Implement validateWebhook() method.
+	}
+
+	protected function processWebhook(array $payload): bool
+	{
+		// TODO: Implement processWebhook() method.
+	}
+
+	protected function extractWebhookId(array $payload): string
+	{
+		// TODO: Implement extractWebhookId() method.
+	}
+
+	/********************************************************************
+	 * CUSTOM POST TYPES
+	********************************************************************/
+	protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void
+	{
+		$queue = JVB()->queue();
+		$queue->registry()->register(
+			self::$syncCustomer,
+			new TypeConfig(
+				executor: $executor,
+				maxRetries: 1,
+			)
+		);
+
+		$queue->registry()->register(
+			self::$import,
+			new TypeConfig(
+				executor: $executor,
+				maxRetries: 2
+			)
+		);
+	}
+
+	public function registerSquarePostTypes():void
+		{
+			$orders = Registrar::forPost($this->orderPostType, 'Square Order', 'Square Orders');
+				$orders->make([
+					'public'	=> true
+				]);
+				$orders->setAll(['system']);
+
+			$fields = $orders->fields();
+			foreach ($this->getOrderFields() as $fieldName => $config) {
+				$fields->addField($fieldName, $config);
+			}
+		}
+			public function getOrderFields():array
+			{
+				return [
 					'post_title' => [
 						'type' => 'text',
 						'label' => 'Order Number'
@@ -341,3152 +507,114 @@
 						'label' => 'Last Updated',
 					]
 				];
-	}
-
-	public function registerSquarePostTypes():void
-	{
-		$orders = Registrar::forPost($this->orderPostType, 'Square Order', 'Square Orders');
-		$orders->make([
-			'public'	=> true
-		]);
-		$orders->setAll(['system']);
-
-		$fields = $orders->fields();
-		foreach ($this->getOrderFields() as $fieldName => $config) {
-			$fields->addField($fieldName, $config);
-		}
-	}
-
-	/**
-	 * Get request headers for API calls
-	 */
-	protected function getRequestHeaders(): array
-	{
-		return [
-			'Authorization' => 'Bearer ' . ($this->credentials['access_token'] ?? ''),
-			'Square-Version' => $this->apiVersion,
-			'Content-Type' => 'application/json'
-		];
-	}
-
-	/**
-	 * Add Square-specific OAuth parameters
-	 */
-	protected function addOAuthParams(array $params): array
-	{
-		// Load current environment setting
-		$this->ensureInitialized();
-
-		// For production, add session=false
-		// For sandbox, session parameter is not supported and should be omitted
-		if ($this->environment === 'production') {
-			$params['session'] = 'false';
-		} else {
-			// Ensure session is not included for sandbox
-			unset($params['session']);
-		}
-
-		return $params;
-	}
-
-	/**
-	 * Exchange OAuth code for tokens
-	 * Override to handle Square's specific response format
-	 */
-	protected function exchangeOAuthCode(string $code): ?array
-	{
-		$this->ensureInitialized();
-
-		// Prepare the request body as an array
-		$body = [
-			'client_id' => $this->credentials['client_id'] ?? '',
-			'client_secret' => $this->credentials['client_secret'] ?? '',
-			'code' => $code,
-			'grant_type' => 'authorization_code',
-			'redirect_uri' => $this->getRedirectUri()
-		];
-
-		$response = wp_remote_post($this->oauth['token'], [
-			'body' => json_encode($body),
-			'headers' => [
-				'Content-Type' => 'application/json',
-				'Square-Version' => $this->apiVersion
-			]
-		]);
-
-		if (is_wp_error($response)) {
-			$this->logError('OAuth token exchange failed', ['error' => $response->get_error_message()]);
-			return null;
-		}
-
-		$data = json_decode(wp_remote_retrieve_body($response), true);
-		if (isset($data['access_token'])) {
-			return [
-				'access_token' => $data['access_token'],
-				'refresh_token' => $data['refresh_token'] ?? '',
-				'expires_in' => $data['expires_in'] ?? 2592000, // 30 days default
-				'token_type' => $data['token_type'] ?? 'Bearer',
-				'merchant_id' => $data['merchant_id'] ?? '',
-				'scope' => $data['scope'] ?? ''
-			];
-		}
-
-		$this->logError('Failed to obtain access token', ['response' => $data]);
-		return null;
-	}
-
-	/**
-	 * Add Square-specific credential data after OAuth
-	 */
-	protected function addCredentialData(array $credentials, array $tokens): array
-	{
-		// Store Square-specific data
-		$credentials['merchant_id'] = $tokens['merchant_id'] ?? '';
-
-		// Preserve selected location if it exists
-		$credentials['location_id'] = $this->credentials['location_id'] ?? '';
-
-		// Preserve sandbox setting
-		$credentials['environment'] = $this->credentials['environment'] ?? 'sandbox';
-
-		return $credentials;
-	}
-
-	/**
-	 * Refresh OAuth token
-	 * Override to handle Square's refresh token flow
-	 */
-	protected function refreshOAuthToken(): bool
-	{
-		if (!$this->isOAuthService || empty($this->credentials['refresh_token'])) {
-			return false;
-		}
-
-		$response = wp_remote_post($this->oauth['token'], [
-			'body' => [
-				'client_id' => $this->credentials['client_id'],
-				'client_secret' => $this->credentials['client_secret'],
-				'refresh_token' => $this->credentials['refresh_token'],
-				'grant_type' => 'refresh_token'
-			],
-			'headers' => $this->getRequestHeaders()
-		]);
-
-		if (is_wp_error($response)) {
-			$this->logError('Failed to refresh Square token', [
-				'error' => $response->get_error_message()
-			]);
-			return false;
-		}
-
-		$data = json_decode(wp_remote_retrieve_body($response), true);
-
-		if (isset($data['access_token'])) {
-			$this->credentials['access_token'] = $data['access_token'];
-			$this->credentials['expires_at'] = time() + ($data['expires_in'] ?? 2592000); // 30 days
-			// Note: Square returns the SAME refresh token
-			if (isset($data['refresh_token'])) {
-				$this->credentials['refresh_token'] = $data['refresh_token'];
 			}
-
-			$this->saveCredentials($this->credentials);
-			return true;
-		}
-
-		return false;
-	}
-
-	/**
-	 * Load Square locations
-	 */
-	protected function loadLocations(): void
+	/********************************************************************
+	 * UTILITY METHODS
+	 ********************************************************************/
+	protected function getEnvironment():string|false
 	{
-		// Skip if we don't have credentials yet (during OAuth flow)
-		if (empty($this->credentials['access_token'])) {
-			return;
+		return $this->loadCredentials()['environment']??false;
+	}
+	protected function getClientID():string|false
+	{
+		return $this->loadCredentials()['client_id']??false;
+	}
+	protected function getClientSecret():string|false
+	{
+		return $this->loadCredentials()['client_secret']??false;
+	}
+	protected function getMerchantID():string|false
+	{
+		return $this->loadCredentials()['merchant_id']??false;
+	}
+	protected function getLocationID():string|false
+	{
+		return $this->loadCredentials()['location_id']??false;
+	}
+	protected function getLocations():array|false
+	{
+		if (!$this->getAccessToken()) {
+			return false;
 		}
 		try {
-			$response = $this->getRequest(
-				'/v2/locations',
-				[],
-				$this->environment
+			return $this->getRequest(
+				sprintf('%s/v2/locations', $this->getBaseURL()),
 			);
 
+		} catch (Exception $e) {
+			$this->logError(
+				'getLocations',
+				'Failed to load locations',
+				[
+					'error'	=> $e->getMessage()
+				]
+			);
+		}
+		return [];
+	}
+		protected function handleGetLocationResponse(WP_Error|array $response, string $method):array
+		{
 			if (isset($response['locations'])) {
-				$this->locations = $response['locations'];
+				$locations = $response['locations'];
 
-				// Update location options in fields
-				$locationOptions = [];
-				foreach ($this->locations as $location) {
-					if ($location['status'] === 'ACTIVE') {
-						$locationOptions[$location['id']] = $location['name'] ?? $location['id'];
+				$options=[];
+				foreach ($locations as $l) {
+					if ($l['status'] === 'ACTIVE') {
+						$options[$l['id']] = $l['name']??$l['id'];
 					}
 				}
-
-				if (!empty($locationOptions)) {
-					$this->fields['location_id']['options'] = $locationOptions;
+				if (!empty($options)) {
+					$this->saveCredentials(['locations' => $options], false);
 				}
+				return $this->response(true, 'Found locations. Stored in table for reference.', $locations);
 			}
-		} catch (Exception $e) {
-			$this->logError('Failed to load locations', ['error' => $e->getMessage()]);
-			// Set empty locations array so we don't retry constantly
-			$this->locations = [];
+			return $this->response(false, 'No locations found.');
 		}
-	}
-
-	protected function renderOAuthConnectedOptions(): void
+		protected function getLocationOptions():array
+		{
+			return $this->loadCredentials()['locations']??[];
+		}
+	protected function getBaseURL():string
 	{
-		if (!$this->isSetUp()) {
-			return;
-		}
-
-		// Check OAuth status
-		$oauth_valid = $this->isOAuthValid();
-
-		if (!$oauth_valid) {
-			?>
-			<div class="oauth-status">
-				<span class="status-invalid">⚠️ OAuth token expired - please reconnect</span>
-			</div>
-			<?php
-		}
-
-		$this->loadLocations();
-		if (!empty($this->locations)) {
-			?>
-			<div class="form-field" data-service="square">
-				<label for="square_location_id">Square Location</label>
-				<select name="location_id"
-						id="square_location_id"
-						class="jvb-ajax-update"
-						data-action="select_location">
-					<option value="">Select a location...</option>
-					<?php foreach ($this->locations as $location): ?>
-						<option value="<?php echo esc_attr($location['id']); ?>"
-							<?php selected($this->credentials['location_id'] ?? '', $location['id']); ?>>
-							<?php echo esc_html($location['name']); ?>
-						</option>
-					<?php endforeach; ?>
-				</select>
-			</div>
-			<?php
-
-			// Show current selection status
-			if (!empty($this->credentials['location'])) {
-				?>
-				<div class="connection-status connected">
-					✅ Connected to: <?php echo esc_html($this->credentials['location_id']); ?>
-				</div>
-				<?php
-			}
-		} else {
-			?>
-			<div class="notice notice-warning">
-				<p>No Square locations found. Please ensure your Square credentials are correct.</p>
-			</div>
-			<?php
-		}
+		return $this->getEnvironment() === 'production'
+			? 'https://connect.squareup.com'
+			: 'https://connect.squareupsandbox.com';
 	}
-
-	/**
-	 * Handle location selection
-	 */
-	public function handleSelectLocation($data): array
+	protected function getAccessToken():string
 	{
-		if (empty($data['location_id'])) {
-			return [
-				'success' => false,
-				'message' => 'No location selected'
-			];
-		}
-
-		$this->credentials['location_id'] = sanitize_text_field($data['location_id']);
-		$this->locationId = $this->credentials['location_id'];
-
-		$result = $this->saveCredentials($this->credentials);
-
-		if ($result['success']) {
-			return [
-				'success' => true,
-				'message' => 'Location updated successfully'
-			];
-		}
-
-		return $result;
+		return $this->loadCredentials()['access_token']??'';
 	}
-
-	/**
-	 * Handle fetching locations
-	 */
-	public function handleFetchLocations(): array
+	/***************************************************************
+	 * ACTIONS
+	***************************************************************/
+	public function handleSelectLocation(array $data):array
 	{
-		try {
-			$this->loadLocations();
-
-			if (empty($this->locations)) {
-				return [
-					'success' => false,
-					'message' => 'No locations found'
-				];
-			}
-
-			return [
-				'success' => true,
-				'data' => $this->locations,
-				'message' => sprintf('Found %d location(s)', count($this->locations))
-			];
-		} catch (Exception $e) {
-			return [
-				'success' => false,
-				'message' => 'Failed to fetch locations: ' . $e->getMessage()
-			];
-		}
-	}
-
-	/**
-	 * Handle webhook verification
-	 */
-	public function handleVerifyWebhook($data): array
-	{
-		if (empty($data['signature']) || empty($data['body'])) {
-			return [
-				'success' => false,
-				'message' => 'Missing signature or body'
-			];
+		if (empty($data['location_id']??'')) {
+			return $this->response(false, 'No location selected');
 		}
 
-		$signatureKey = $this->credentials['webhook_signature_key'] ?? '';
-
-		if (empty($signatureKey)) {
-			return [
-				'success' => false,
-				'message' => 'Webhook signature key not configured'
-			];
-		}
-
-		// Calculate expected signature
-		$stringToSign = $data['url'] . $data['body'];
-		$expectedSignature = base64_encode(
-			hash_hmac('sha256', $stringToSign, $signatureKey, true)
-		);
-
-		$isValid = hash_equals($expectedSignature, $data['signature']);
-
-		return [
-			'success' => $isValid,
-			'message' => $isValid ? 'Valid webhook signature' : 'Invalid webhook signature'
+		$credentials = [
+			'location_id'	=> sanitize_text_field($data['location_id'])
 		];
+		return $this->saveCredentials($credentials, false);
 	}
 
-	/**
-	 * Test payment/connection
-	 */
-	public function handleTestPayment(): array
+	public function handleFetchLocations():array
 	{
 		try {
-			// Test with a simple API call to verify connection
-			$response = $this->getRequest(
-				'/v2/locations/'.$this->locationId,
-				[],
-				$this->environment
+			$locations = $this->getLocations();
+			if (!$locations) {
+				return $this->response(false, 'No locations found');
+			}
+			return $this->response(
+				true,
+				sprintf('Found %d location(s)', count($locations)),
+				$locations
 			);
-			if (!empty($response['location'])) {
-				return [
-					'success' => true,
-					'message' => 'Successfully connected to Square',
-					'data' => [
-						'location_name' => $response['location']['name'] ?? 'Unknown',
-						'merchant_id' => $this->merchantId,
-						'environment' => $this->environment
-					]
-				];
-			}
-
-			return [
-				'success' => false,
-				'message' => 'Connection test failed'
-			];
 		} catch (Exception $e) {
-			return [
-				'success' => false,
-				'message' => 'Connection test failed: ' . $e->getMessage()
-			];
-		}
-	}
-
-	/**
-	 * Create a Square customer
-	 */
-	public function createCustomer(array $customerData): array
-	{
-		try {
-			$response = $this->postRequest('/v2/customers', [
-				'given_name' => $customerData['first_name'] ?? '',
-				'family_name' => $customerData['last_name'] ?? '',
-				'email_address' => $customerData['email'] ?? '',
-				'phone_number' => $customerData['phone'] ?? '',
-				'reference_id' => $customerData['reference_id'] ?? '',
-				'note' => $customerData['note'] ?? ''
-			], $this->environment);
-
-			return [
-				'success' => true,
-				'data' => $response['customer'] ?? []
-			];
-		} catch (Exception $e) {
-			return [
-				'success' => false,
-				'message' => 'Failed to create customer: ' . $e->getMessage()
-			];
-		}
-	}
-
-	/**
-	 * Create a catalog item
-	 */
-	public function createCatalogItem(array $itemData): array
-	{
-		try {
-			$response = $this->postRequest('/v2/catalog/object',  [
-				'idempotency_key' => wp_generate_uuid4(),
-				'object' => [
-					'type' => 'ITEM',
-					'id' => '#' . sanitize_title($itemData['name']),
-					'item_data' => [
-						'name' => $itemData['name'],
-						'description' => $itemData['description'] ?? '',
-						'variations' => $this->buildItemVariations($itemData)
-					]
-				]
-			], $this->environment);
-
-			return [
-				'success' => true,
-				'data' => $response['catalog_object'] ?? []
-			];
-		} catch (Exception $e) {
-			return [
-				'success' => false,
-				'message' => 'Failed to create catalog item: ' . $e->getMessage()
-			];
-		}
-	}
-
-	/**
-	 * Build item variations for catalog
-	 */
-	protected function buildItemVariations(array $itemData): array
-	{
-		$variations = [];
-
-		if (isset($itemData['variations']) && is_array($itemData['variations'])) {
-			foreach ($itemData['variations'] as $variation) {
-				$variations[] = [
-					'type' => 'ITEM_VARIATION',
-					'id' => '#' . sanitize_title($variation['name']),
-					'item_variation_data' => [
-						'name' => $variation['name'],
-						'pricing_type' => 'FIXED_PRICING',
-						'price_money' => [
-							'amount' => $this->formatPrice($variation['_square_price']??0),
-							'currency' => $this->getCurrency()
-						]
-					]
-				];
-			}
-		} else {
-			// Default single variation
-			$variations[] = [
-				'type' => 'ITEM_VARIATION',
-				'id' => '#regular',
-				'item_variation_data' => [
-					'name' => 'Regular',
-					'pricing_type' => 'FIXED_PRICING',
-					'price_money' => [
-						'amount' => $this->formatPrice($itemData['_square_price'] ?? 0),
-						'currency' => $this->getCurrency()
-					]
-				]
-			];
-		}
-
-		return $variations;
-	}
-
-	/**
-	 * Handle OAuth disconnect
-	 */
-	public function handleOAuthDisconnect(): array
-	{
-		try {
-			// Revoke the token with Square
-			if (!empty($this->credentials['access_token'])) {
-				wp_remote_post($this->oauth['revoke'], [
-					'body' => [
-						'client_id' => $this->credentials['client_id'] ?? '',
-						'access_token' => $this->credentials['access_token']
-					],
-					'headers' => $this->getRequestHeaders()
-				]);
-			}
-
-			// Clear stored credentials
-			$this->credentials = [
-				'client_id' => $this->credentials['client_id'] ?? '',
-				'client_secret' => $this->credentials['client_secret'] ?? ''
-			];
-
-			$this->saveCredentials($this->credentials);
-			$this->clearCache();
-
-			return [
-				'success' => true,
-				'message' => 'Successfully disconnected from Square'
-			];
-		} catch (Exception $e) {
-			return [
-				'success' => false,
-				'message' => 'Failed to disconnect: ' . $e->getMessage()
-			];
-		}
-	}
-
-	/**
-	 * Register additional WordPress hooks
-	 */
-	protected function registerAdditionalHooks(): void
-	{
-		if (!$this->isSetUp()) {
-			error_log('Square is not setup');
-			return;
-		}
-
-        add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
-
-		add_filter('jvbAdditionalActions', [Checkout::class, 'render']);
-
-		add_filter('jvb_checkout_description', function (string $desc, string $provider) {
-			if ($provider === 'square') {
-				return 'Securely checkout with your name, email, and payments processed by Square.';
-			}
-			return $desc;
-		}, 10, 2);
-
-		// Square-specific pickup fields (extracted from old outputCheckout)
-		add_filter('jvb_checkout_fields', [$this, 'addPickupFields'], 10, 2);
-
-		// Browse URL for this client (restaurant menu)
-		add_filter('jvb_checkout_browse_url', function () {
-			return get_post_type_archive_link(BASE . 'menu_item');
-		});
-		add_filter('jvb_checkout_browse_text', function () {
-			return 'browse our menu';
-		});
-	}
-
-	/**
-	 * Pickup/ordering fields for the shared checkout form.
-	 * Specific to this Square client's food ordering use case.
-	 */
-	public function addPickupFields(string $html, string $provider): string
-	{
-		if ($provider !== 'square') {
-			return $html;
-		}
-
-		return $html
-			. '<h3>Pickup Details</h3>'
-			. Form::render('pickup_time', null, [
-				'type'     => 'datetime',
-				'label'    => 'Pickup Time',
-				'min'      => '11:00',
-				'max'      => '20:00',
-				'required' => true,
-			])
-			. Form::render('special_instructions', null, [
-				'type'  => 'textarea',
-				'label' => 'Special Instructions',
-				'quill' => true,
-			]);
-	}
-
-	protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void
-	{
-		$queue    = JVB()->queue();
-
-		$queue->registry()->register(self::$syncCustomer, new TypeConfig(
-			executor:   $executor,
-			maxRetries: 2
-		));
-
-		$queue->registry()->register(self::$import, new TypeConfig(
-			executor:   $executor,
-			maxRetries: 3
-		));
-	}
-
-	/******************************************************************
-	 * POST SYNC METHODS
-	 ******************************************************************/
-
-	/**
-	 * Handle post save for Square sync
-	 */
-	protected function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings): void
-	{
-		error_log('==== [Square]::handleTheSavePost ====');
-		$this->queueOperation(self::$syncTo, [
-			'items'   => [$postID],
-			'user'		=> user_can($post->post_author, 'manage_options') ? null : $post->post_author
-		], [
-			'priority' => 'high',
-			'delay'    => 30,
-		]);
-		Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued');
-	}
-
-	/**
-	 * Handle post deletion
-	 */
-	public function handleDeletePost(int $postID): void
-	{
-		$item_id = $this->getServiceItemID($postID);
-
-		if (empty($item_id)) {
-			return;
-		}
-		$this->queueOperation(self::$deleteFrom, [
-			'external_ids' => [$item_id],
-			'post_id'      => $postID,
-		], [
-			'priority' => 'high',
-		]);
-
-	}
-
-	/**
-	 * Process sync to Square
-	 */
-	private function processSyncToSquare(array $data): array
-	{
-		$items = $data['items'] ?? [];
-		$success = [];
-		$errors = [];
-
-		if (empty($items)) {
-			return [
-				'success' => true,
-				'result' => [
-					'synced' => [],
-					'errors' => ['No items to sync']
-				]
-			];
-		}
-
-		//Check images are already uploaded first
-		$image_mappings = $this->batchUploadImages($items);
-
-		$catalog = [];
-		$map = [];
-		foreach ($items as $postID) {
-			$post = get_post($postID);
-			if (!$post) {
-				$errors[] = "Post $postID not found";
-				continue;
-			}
-
-			$square_image_id = $image_mappings[$postID] ?? null;
-			$catalog_object = $this->buildCatalogObject($postID, $square_image_id);
-			if (is_wp_error($catalog_object)) {
-				$errors[] = $catalog_object->get_error_message();
-				continue;
-			}
-			$map[$catalog_object['id']] = $postID;
-			$catalog[] = $catalog_object;
-		}
-
-		if (!empty($catalog)) {
-			$response = $this->postRequest('catalog/batch-upsert', [
-				'idempotency_key' => wp_generate_uuid4(),
-				'batches' => [[
-					'objects' => $catalog
-				]]
-			]);
-
-			if (!is_wp_error($response)) {
-				$this->processBatchSyncResponse($response, $map, $success, $errors);
-			} else {
-				// Handle batch request failure
-				$error_message = 'Batch sync failed';
-				$this->logError($error_message, [
-					'method' => 'processSyncToSquare',
-					'post_ids' => $items,
-					'error' => $response
-				]);
-
-				// Mark all items as failed
-				foreach ($items as $postID) {
-					if (get_post($postID)) { // Only if post exists
-						$errors[] = "Failed to sync post $postID";
-						update_post_meta($postID, BASE . '_square_sync_status', 'error');
-					}
-				}
-			}
-		}
-
-		return [
-			'success' => count($success) > 0,
-			'result' => [
-				'synced' => $success,
-				'errors' => $errors
-			]
-		];
-	}
-
-	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) {
-				$object_id = $object['id'];
-				$post_id = $map[$object_id] ?? null;
-
-				if (!$post_id) {
-					// Try to find post ID from the object ID pattern
-					if (preg_match('/#\w+_(\d+)/', $object_id, $matches)) {
-						$post_id = (int)$matches[1];
-					}
-				}
-
-				if ($post_id && get_post($post_id)) {
-					// Update post meta with Square data
-					update_post_meta($post_id, BASE . '_square_catalog_id', $object['id']);
-					update_post_meta($post_id, BASE . '_square_sync_status', 'synced');
-					update_post_meta($post_id, BASE . '_square_last_sync', current_time('mysql'));
-
-					// Save variation IDs if present
-					if (!empty($object['item_data']['variations'])) {
-						foreach ($object['item_data']['variations'] as $index => $variation) {
-							update_post_meta($post_id, BASE . '_square_variation_' . $index . '_id', $variation['id']);
-						}
-					}
-
-					$success[] = $post_id;
-				}
-			}
-		}
-
-		// Handle errors
-		if (!empty($response['errors'])) {
-			foreach ($response['errors'] as $error) {
-				$error_detail = $error['detail'] ?? 'Unknown error';
-				$error_field = $error['field'] ?? '';
-
-				// Try to extract post ID from error field if possible
-				$post_id = null;
-				if (preg_match('/batches\[0\]\.objects\[(\d+)\]/', $error_field, $matches)) {
-					$object_index = (int)$matches[1];
-					// Get post ID from the object at this index
-					if (isset($catalog_objects[$object_index])) {
-						$object_id = $catalog_objects[$object_index]['id'];
-						$post_id = $map[$object_id] ?? null;
-					}
-				}
-
-				$error_message = $post_id
-					? "Post $post_id: $error_detail"
-					: "Batch error: $error_detail";
-
-				$errors[] = $error_message;
-
-				if ($post_id) {
-					update_post_meta($post_id, BASE . '_square_sync_status', 'error');
-				}
-			}
-		}
-
-		// Handle any posts that weren't in the response (shouldn't happen but good to check)
-		foreach ($map as $object_id => $post_id) {
-			if (!in_array($post_id, $success)) {
-				$found_in_errors = false;
-				foreach ($errors as $error) {
-					if (str_contains($error, "Post $post_id")) {
-						$found_in_errors = true;
-						break;
-					}
-				}
-
-				if (!$found_in_errors) {
-					$errors[] = "Post $post_id: No response from Square";
-					update_post_meta($post_id, BASE . '_square_sync_status', 'error');
-				}
-			}
-		}
-	}
-
-	/**
-	 * Build catalog object from WordPress post
-	 *
-	 * @param int $postID WordPress post ID
-	 * @param string|null $square_image_id Previously uploaded Square image ID
-	 * @return array|WP_Error Catalog object or error
-	 */
-	//TODO: Get to work with Registrar settings
-	protected function buildCatalogObject(int $postID, ?string $square_image_id = null): array|WP_Error
-	{
-		$post = get_post($postID);
-		if (!$post) {
-			return new WP_Error('post_not_found', "Post $postID not found");
-		}
-
-		$meta = Meta::forPost($postID);
-		$post_type = get_post_type($postID);
-
-		// 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 = [
-			'type' => 'ITEM',
-			'id' => $existing_square_id ?: '#menu_item_item_' . $postID,
-			'item_data' => [
-				'name' => $post->post_title,
-				'description' => wp_strip_all_tags($post->post_content),
-				'product_type' => $product_type,
-				'variations' => [],
-				'is_taxable' => true,
-			]
-		];
-
-		// Add image ID if provided (must be already uploaded to Square)
-		if (!empty($square_image_id)) {
-			$catalog_object['item_data']['image_ids'] = [$square_image_id];
-		}
-
-		// Add variations
-		$variations = $meta->get('_square_product_variations');
-		if (empty($variations)) {
-			// Create default variation if none exist
-			$catalog_object['item_data']['variations'][] = [
-				'type' => 'ITEM_VARIATION',
-				'id' => $existing_square_id ? null : '#'.BASE.'menu_item_' . $postID . '_var_default',
-				'item_variation_data' => [
-					'name' => 'Regular',
-					'ordinal' => 0,
-					'pricing_type' => 'FIXED_PRICING',
-					'price_money' => [
-						'amount' => $this->formatPrice($meta->get('_square_price')),
-						'currency' => $this->getCurrency()
-					],
-					'sellable' => true,
-					'stockable' => true
-				]
-			];
-		} else {
-			$resetVariations = false;
-			foreach ($variations as $index => $variation) {
-				$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' => $variation['item_id'],
-					'item_variation_data' => [
-						'name' => $variation['name'] ?? 'Variation ' . ($index + 1),
-						'ordinal' => $index,
-						'pricing_type' => 'FIXED_PRICING',
-						'price_money' => [
-							'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
-		$categories = wp_get_post_terms($postID, $post_type . '_category', ['fields' => 'ids']);
-		if (!empty($categories)) {
-			$category_ids = [];
-			foreach ($categories as $term_id) {
-				$square_cat_id = get_term_meta($term_id, BASE . '_square_category_id', true);
-				if ($square_cat_id) {
-					$category_ids[] = $square_cat_id;
-				}
-			}
-			if (!empty($category_ids)) {
-				$catalog_object['item_data']['category_ids'] = $category_ids;
-			}
-		}
-
-		// Add modifiers if they exist
-		$modifiers = $meta->get('modifiers');
-		if (!empty($modifiers)) {
-			$modifier_ids = [];
-			foreach ($modifiers as $modifier) {
-				if (!empty($modifier['square_id'])) {
-					$modifier_ids[] = $modifier['square_id'];
-				}
-			}
-			if (!empty($modifier_ids)) {
-				$catalog_object['item_data']['modifier_list_info'] = array_map(function($id) {
-					return ['modifier_list_id' => $id];
-				}, $modifier_ids);
-			}
-		}
-
-		// Add tax settings
-		$tax_ids = $meta->get('tax_ids');
-		if (!empty($tax_ids)) {
-			$catalog_object['item_data']['tax_ids'] = $tax_ids;
-		}
-
-		return $catalog_object;
-	}
-
-
-
-	/**
-	 * Get variation mapping for post type
-	 */
-	protected function getVariationMapping(string $post_type): array
-	{
-		$registrar = Registrar::getInstance($post_type);
-		if (!$registrar) {
-			return [];
-		}
-		$config = $registrar->getIntegrationConfig($this->service_name);
-		$product_type = $config['content_type']??'REGULAR';
-		$valid_fields = $this->getValidFieldsForProductType($product_type);
-
-		$defaults = [
-			'name' => 'name',
-			'id' => '_square_item_id',
-			'sku' => '_square_sku',
-			'price' => '_square_price',
-			'track_inventory' => '_square_track_inventory',
-			'service_duration' => '_square_service_duration',
-			'available_for_booking' => '_square_available_for_booking',
-			'gift_card_type' => '_square_gift_card_type',
-			'ingredients' => '_square_ingredients',
-			'preparation_time_duration' => '_square_preparation_time_duration'
-		];
-
-		$extended = apply_filters(
-			BASE . $this->service_name . '_variation_mapping',
-			$defaults,
-			$post_type,
-			$this
-		);
-
-		return array_filter($extended, function($key) use ($valid_fields) {
-			return in_array($key, $valid_fields);
-		}, ARRAY_FILTER_USE_KEY);
-	}
-
-
-
-	/**
-	 * Get or create Square category
-	 */
-	private function getOrCreateSquareCategory(string $name): ?string
-	{
-		// Check cached mapping
-		$cached_id = get_option(BASE . 'square_category_' . sanitize_title($name));
-		if ($cached_id) {
-			return $cached_id;
-		}
-
-		// Create new category
-		$response = $this->postRequest('catalog/batch-upsert', [
-			'idempotency_key' => wp_generate_uuid4(),
-			'batches' => [[
-				'objects' => [[
-					'type' => 'CATEGORY',
-					'id' => '#category_' . sanitize_title($name),
-					'category_data' => [
-						'name' => $name
-					]
-				]]
-			]]
-		]);
-
-		if (!is_wp_error($response) && !empty($response['objects'][0]['id'])) {
-			$category_id = $response['objects'][0]['id'];
-			update_option(BASE . 'square_category_' . sanitize_title($name), $category_id);
-			return $category_id;
-		}
-
-		return null;
-	}
-
-	/**
-	 * Get field mapping for post type
-	 */
-	protected function getFieldMapping(string $post_type): array
-	{
-		$registrar = Registrar::getInstance($post_type);
-		if (!$registrar) {
-			return [];
-		}
-		$config = $registrar->getIntegrationConfig($this->service_name);
-		$product_type = $config['content_type']??'REGULAR';
-		$valid_fields = $this->getValidFieldsForProductType($product_type);
-
-		$defaults = [
-			'name' => 'post_title',
-			'description_html' => 'post_content',
-			'abbreviation' => 'abbreviation',
-			'id' => '_square_catalog_id',
-			'sku' => '_square_sku',
-			'category_id' => 'category',
-			'image_ids' => 'post_thumbnail',
-			'price' => '_square_price',
-			'tax_ids' => 'tax_ids',
-			// Availability
-			'available_online' => '_square_available_online',
-			'available_for_pickup' => '_square_available_for_pickup',
-			'available_electronically' => '_square_available_electronically',
-			// Modifiers
-			'modifier_list_info' => 'modifiers',
-			// Item options
-			'item_options' => 'options',
-			// Reporting
-			'reporting_category' => 'reporting_category',
-		];
-
-		$extended = apply_filters(
-			BASE . $this->service_name . '_field_mapping',
-			$defaults,
-			$post_type,
-			$this
-		);
-
-		return array_filter($extended, function($key) use ($valid_fields) {
-			return in_array($key, $valid_fields);
-		}, ARRAY_FILTER_USE_KEY);
-	}
-
-	/**
-	 * Get valid fields for Square product type
-	 */
-	//TODO: This feels redundant now, with how we've defined fields in getAdditionalFields
-	private function getValidFieldsForProductType(string $product_type): array
-	{
-		$fields = ['name', 'description_html', 'sku', 'price', 'image_ids', 'category_id'];
-
-		$type_specific = [
-			'FOOD_AND_BEV' => [
-				'ingredients',
-				'preparation_time_duration',
-				'dietary_preferences',
-				'calories_text'
-			],
-			'APPOINTMENTS_SERVICE' => [
-				'service_duration',
-				'available_for_booking',
-				'team_member_ids',
-				'booking_availability'
-			],
-			'EVENT' => [
-				'start_date',
-				'end_date',
-				'venue_details',
-				'capacity'
-			],
-			'GIFT_CARD' => [
-				'gift_card_type',
-				'allowed_locations'
-			]
-		];
-
-		if (isset($type_specific[$product_type])) {
-			$fields = array_merge($fields, $type_specific[$product_type]);
-		}
-
-		return $fields;
-	}
-
-	/******************************************************************
-	 * CUSTOMER MANAGEMENT
-	 ******************************************************************/
-
-	/**
-	 * Handle customer authentication during checkout
-	 */
-	//TODO: Is this necessary?
-	public function handleCustomerAuth($data):WP_Error|array
-	{
-		$email = sanitize_email($data['email'] ?? '');
-		$action = sanitize_text_field($data['action_type'] ?? 'check');
-
-		if (!$email) {
-			return new WP_Error('error', 'Email required');
-		}
-
-		switch ($action) {
-			case 'check':
-				return $this->checkCustomerExists($email);
-			case 'create':
-				return $this->createCustomerAccount($email);
-			default:
-				$this->logError('No action set for customer auth: '.$action, [
-					'method' => 'handleCustomerAuth'
-				]);
-				return new WP_Error('error', 'No action set for customer auth');
-		}
-
-	}
-
-
-
-	/**
-	 * Check if customer exists
-	 */
-	private function checkCustomerExists(string $email):WP_Error|array
-	{
-		// Check WordPress user
-		$user = get_user_by('email', $email);
-
-		if ($user) {
-			// Check if user has Square customer integration
-			$square_customer_id = get_user_meta($user->ID, BASE . '_square_customer_id', true);
-
-			if ($square_customer_id) {
-				return [
-					'exists'	=> true,
-					'has_account'	=> true,
-					'message' => 'Account found. Please enter your password to continue'
-				];
-			} else {
-				return [
-					'exists' => true,
-					'has_account' => false,
-					'message' => 'Email found. Would you like to create an account to save your order history?'
-				];
-			}
-		}
-
-			// Check Square for customer
-			$response = $this->postRequest('customers/search', [
-				'filter' => [
-					'email_address' => [
-						'exact' => $email
-					]
-				]
-			]);
-
-			if (!is_wp_error($response) && !empty($response['customers'])) {
-				return [
-					'exists' => true,
-					'has_account' => false,
-					'square_only' => true,
-					'message' => 'Found your previous orders. Create an account to access them?'
-				];
-			} else {
-				return [
-					'exists' => false,
-					'message' => 'New customer'
-				];
-			}
-	}
-
-	private function createCustomerAccount(string $email):WP_Error|array
-	{
-		// Validate email format
-		if (!is_email($email)) {
-			return new WP_Error('error', 'Invalid email address');
-		}
-
-		// Check if user already exists
-		if (email_exists($email)) {
-			return [
-				'success' => false,
-				'exists' => true,
-				'message' => 'An account with this email already exists. Please log in instead.'
-			];
-		}
-
-		// Create user account without password (they'll set it via email)
-		$user_id = wp_create_user(
-			$email,
-			wp_generate_password(20, true, true), // Temporary random password
-			$email
-		);
-
-		if (is_wp_error($user_id)) {
-			$this->logError('Failed to create customer account', [
-				'email' => $email,
-				'error' => $user_id->get_error_message()
-			]);
-			return new WP_Error('error', 'Failed to create account. Please try again.');
-		}
-
-		// Set user role (assuming you have a customer role defined)
-		$user = new WP_User($user_id);
-		$user->set_role(BASE.'foodie');
-
-		// Generate password reset key
-		$reset_key = get_password_reset_key($user);
-
-		if (is_wp_error($reset_key)) {
-			return new WP_Error('error', 'Account created, but couldn\'t send email. Please use password reset.');
-		}
-
-
-		// Send welcome email with password setup link
-		$this->sendWelcomeEmail($user, $reset_key);
-
-		// Link to Square customer if exists
-		$square_customer_id = $this->getOrCreateSquareCustomer([
-			'email' => $email,
-			'name' => $email
-		]);
-
-		if ($square_customer_id) {
-			update_user_meta($user_id, BASE . '_square_customer_id', $square_customer_id);
-		}
-
-		return [
-			'success' => true,
-			'message' => 'Account created! Check your email to set your password.',
-			'user_id' => $user_id
-		];
-	}
-
-	/**
-	 * Send welcome email with password setup
-	 */
-	private function sendWelcomeEmail(WP_User $user, string $reset_key): void
-	{
-		$site_name = get_bloginfo('name');
-		$reset_url = get_home_url(null, "wp-login.php?action=rp&key=$reset_key&login=" . rawurlencode($user->user_login), 'login');
-
-		$message = sprintf(
-			"Welcome to %s!\n\n" .
-			"Your account has been created. Please click the button below to set your password:\n\n" .
-			"%s\n\n" .
-			"Or, copy and paste the link below:\n\n".
-			"%s\n\n" .
-			"Once you've set your password, you can:\n" .
-			"- View your order history\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",
-			$site_name,
-			JVB()->email()->button('Reset Password', $reset_url),
-			JVB()->email()->link($reset_url),
-		);
-
-		JVB()->email()->sendEmail(
-			$user->user_email,
-			sprintf('[%s] Welcome! Set Your Password', $site_name),
-			$message
-		);
-	}
-
-	/******************************************************************
-	 * WEBHOOK HANDLING
-	 ******************************************************************/
-
-	/**
-	 * Validate webhook signature
-	 */
-	protected function validateWebhook(array $payload): bool
-	{
-		// Get signature from headers
-		$signature = $_SERVER['HTTP_X_SQUARE_SIGNATURE'] ?? '';
-
-		// If no signature provided by Square, validation fails
-		if (empty($signature)) {
-			$this->logDebug('No webhook signature provided by Square');
-			return false;
-		}
-
-		// If webhook signature key is not configured, we can't validate
-		// but we might want to process the webhook anyway (less secure)
-		if (empty($this->webhook_signature_key)) {
-			$this->logDebug('Webhook signature key not configured - processing without validation');
-			// You might want to return false here for stricter security
-			// return false;
-			return true; // Process webhook but log that it's unverified
-		}
-
-		// Get the raw request body
-		$body = file_get_contents('php://input');
-
-		// Calculate expected signature
-		$expected = base64_encode(hash_hmac('sha256', $body, $this->webhook_signature_key, true));
-
-		// Use timing-safe comparison
-		$is_valid = hash_equals($expected, $signature);
-
-		if (!$is_valid) {
-			$this->logError('Invalid webhook signature', [
-				'provided' => substr($signature, 0, 10) . '...',
-				'expected' => substr($expected, 0, 10) . '...'
-			]);
-		}
-
-		return $is_valid;
-	}
-
-	/**
-	 * Process webhook event
-	 */
-	protected function processWebhook(array $payload): bool
-	{
-		$event_type = $payload['type'] ?? '';
-		$data = $payload['data'] ?? [];
-
-		switch ($event_type) {
-			case 'payment.created':
-			case 'payment.updated':
-				return $this->handlePaymentWebhook($data);
-
-			case 'order.created':
-			case 'order.updated':
-			case 'order.fulfillment.updated':
-				return $this->handleOrderWebhook($data);
-
-			case 'catalog.version.updated':
-				return $this->handleCatalogWebhook($data);
-
-			case 'customer.created':
-			case 'customer.updated':
-				return $this->handleCustomerWebhook($data);
-
-			default:
-				$this->logDebug('Unhandled webhook type', ['type' => $event_type]);
-				return true;
-		}
-	}
-
-	/**
-	 * Handle order status webhook
-	 */
-	private function handleOrderWebhook(array $data): bool
-	{
-		$order_id = $data['object']['order']['id'] ?? '';
-		$state = $data['object']['order']['state'] ?? '';
-		$fulfillments = $data['object']['order']['fulfillments'] ?? [];
-
-		if (!$order_id) {
-			return false;
-		}
-
-		// Find the WP post for this order
-		$wp_order_id = $this->getOrderPost($order_id);
-
-		if ($wp_order_id) {
-			// Update the post meta
-			$meta = Meta::forPost($wp_order_id);
-			$updates = [
-				'status' => $state,
-				'updated_at' => current_time('mysql')
-			];
-
-			// Extract fulfillment status and pickup time
-			if (!empty($fulfillments[0])) {
-				$fulfillment = $fulfillments[0];
-				$updates['fulfillment_status'] = $fulfillment['state'] ?? $state;
-
-				if (!empty($fulfillment['pickup_details']['pickup_at'])) {
-					$updates['pickup_time'] = $fulfillment['pickup_details']['pickup_at'];
-				}
-			}
-
-			$meta->setAll($updates);
-
-			// Trigger notification to customer if order is ready
-			if ($state === 'PREPARED') {
-				do_action(BASE . 'square_order_ready', $wp_order_id, $order_id);
-			}
-		}
-		// Trigger action for other integrations
-		do_action(BASE . 'square_order_updated', $order_id, $state, $data);
-
-		return true;
-	}
-
-	/******************************************************************
-	 * CONNECTION TESTING
-	 ******************************************************************/
-	protected function performConnectionTest(): bool
-	{
-		try {
-			$response = $this->getRequest('/v2/merchants/me');
-			if (is_wp_error($response)) {
-				$this->logError('Connection test failed', ['error' => $response->get_error_message()]);
-				return false;
-			}
-
-			// Check if we got valid merchant data
-			return !empty($response['merchant']);
-		} catch (Exception $e) {
-			$this->logError('Connection test failed', ['error' => $e->getMessage()]);
-			return false;
-		}
-	}
-
-	/**
-	 * Check OAuth connection status
-	 */
-	protected function checkOAuthStatus(): array
-	{
-		if (!$this->hasOAuthCredentials()) {
-			return [
-				'success' => false,
-				'message' => 'OAuth not configured. Please authorize with Square.',
-				'authorized' => false
-			];
-		}
-
-		// Test the connection
-		if ($this->performConnectionTest()) {
-			$merchant = $this->getRequest('merchants/me');
-			return [
-				'success' => true,
-				'message' => 'Successfully connected to Square',
-				'authorized' => true,
-				'merchant' => $merchant['merchant'] ?? []
-			];
-		}
-
-		return [
-			'success' => false,
-			'message' => 'OAuth token may be expired. Please re-authorize.',
-			'authorized' => false
-		];
-	}
-
-	/******************************************************************
-	 * ADMIN UI
-	 ******************************************************************/
-	/**
-	 * Enqueue checkout scripts with Square configuration
-	 */
-	public function enqueueScripts(): void
-	{
-		jvbInlineStyles('forms');
-		$this->loadCredentials();
-		$sdk_url = $this->environment === 'production'
-			? 'https://web.squarecdn.com/v1/square.js'
-			: 'https://sandbox.web.squarecdn.com/v1/square.js';
-
-		wp_enqueue_script(
-			'square-payments-sdk',
-			$sdk_url,
-			[],
-			null,
-			['strategy' => 'defer', 'in_footer' => true]
-		);
-
-		// Shared cart checkout base class
-		wp_register_script(
-			'jvb-checkout',
-			JVB_URL . 'assets/js/min/checkout.min.js',
-			['jvb-utility', 'jvb-queue', 'jvb-a11y', 'jvb-cache', 'jvb-tabs', 'jvb-popup', 'jvb-login'],
-			'1.1.32',
-			['strategy' => 'defer', 'in_footer' => true]
-		);
-
-		// Square checkout extends CartCheckout
-		wp_register_script(
-			'jvb-square-checkout',
-			JVB_URL . 'assets/js/min/square.min.js',
-			['jvb-checkout', 'square-payments-sdk'],
-			'1.1.32',
-			['strategy' => 'defer', 'in_footer' => true]
-		);
-
-		wp_enqueue_script('jvb-square-checkout');
-
-		wp_localize_script('jvb-square-checkout', 'squareConfig', [
-			'isOpen'         => jvbIsOpen()?'1':'0',
-			'application_id' => $this->credentials['client_id'] ?? '',
-			'location_id'    => $this->locationId,
-			'environment'    => $this->environment,
-			'currency'       => $this->getCurrency(),
-			'is_logged_in'   => is_user_logged_in(),
-			'user_email'     => is_user_logged_in() ? wp_get_current_user()->user_email : '',
-		]);
-	}
-
-	/******************************************************************
-	 * HELPER METHODS
-	 ******************************************************************/
-
-	/**
-	 * Get or create Square customer
-	 */
-	public function getOrCreateSquareCustomer(array $customer_info): ?string
-	{
-		if (empty($customer_info['email'])) {
-			return null;
-		}
-
-		// Search for existing customer
-		$search_response = $this->postRequest('customers/search', [
-			'filter' => [
-				'email_address' => [
-					'exact' => $customer_info['email']
-				]
-			]
-		]);
-
-		if (!is_wp_error($search_response) && !empty($search_response['customers'])) {
-			return $search_response['customers'][0]['id'];
-		}
-
-		// Create new customer
-		$create_response = $this->postRequest('customers', [
-			'given_name' => $customer_info['name'] ?? '',
-			'email_address' => $customer_info['email'],
-			'phone_number' => $customer_info['phone'] ?? ''
-		]);
-
-		if (!is_wp_error($create_response) && !empty($create_response['customer']['id'])) {
-			return $create_response['customer']['id'];
-		}
-
-		return null;
-	}
-
-
-	/**
-	 * Process delete from Square
-	 */
-	private function processDeleteFromSquare(array $data): array
-	{
-		$square_ids = $data['square_ids'] ?? [];
-
-		if (empty($square_ids)) {
-			return [
-				'success' => false,
-				'result' => ['error' => 'No Square IDs provided']
-			];
-		}
-
-		$response = $this->postRequest('catalog/batch-delete', [
-			'object_ids' => $square_ids
-		]);
-
-		if (is_wp_error($response)) {
-			return [
-				'success' => false,
-				'result' => ['error' => $response->get_error_message()]
-			];
-		}
-
-		// Clean up post meta
-		if (!empty($data['post_id'])) {
-			delete_post_meta($data['post_id'], BASE . '_square_catalog_id');
-			delete_post_meta($data['post_id'], BASE . '_square_sync_status');
-		}
-
-		return [
-			'success' => true,
-			'result' => ['deleted' => $square_ids]
-		];
-	}
-
-	/**
-	 * Process sync from Square
-	 */
-	private function processSyncFromSquare(array $data): array
-	{
-		$cursor = $data['cursor'] ?? null;
-		$types = $data['types'] ?? ['ITEM'];
-
-		$request_data = [
-			'types' => implode(',', $types),
-			'limit' => 100
-		];
-
-		if ($cursor) {
-			$request_data['cursor'] = $cursor;
-		}
-
-		$response = $this->getRequest('catalog/list?' . http_build_query($request_data));
-
-		if (is_wp_error($response)) {
-			return [
-				'success' => false,
-				'result' => ['error' => $response->get_error_message()]
-			];
-		}
-
-		$imported = 0;
-		$errors = [];
-
-		foreach ($response['objects'] ?? [] as $object) {
-			if ($object['type'] === 'ITEM') {
-				$result = $this->importSquareItem($object);
-				if ($result) {
-					$imported++;
-				} else {
-					$errors[] = $object['id'];
-				}
-			}
-		}
-
-		// Queue next batch if cursor exists
-		if (!empty($response['cursor'])) {
-			$this->queueOperation('sync_from_square', [
-				'cursor' => $response['cursor'],
-				'types' => $types
-			]);
-		}
-
-		return [
-			'success' => true,
-			'result' => [
-				'imported' => $imported,
-				'errors' => $errors,
-				'has_more' => !empty($response['cursor'])
-			]
-		];
-	}
-
-	/**
-	 * Import Square item to WordPress
-	 */
-	private function importSquareItem(array $item): bool|int
-	{
-		//TODO: We need to add the post type to custom meta for Square, this is not good if we have multiple post types with the same product type
-		// Find matching content type
-		$product_type = $item['item_data']['product_type'] ?? 'REGULAR';
-		$post_type = null;
-
-		foreach (Registrar::getRegistered() as $registrar) {
-			if (!$registrar->hasIntegration($this->service_name)) {
-				continue;
-			}
-			$config = $registrar->getIntegration($this->service_name);
-			if ($config->getContent_type() && $config->getContent_type() === $product_type) {
-				$post_type = jvbCheckBase($registrar->getSlug());
-				break;
-			}
-
-		}
-
-		if (!$post_type) {
-			return false;
-		}
-
-		// Check if item already exists
-		$existing = get_posts([
-			'post_type' => $post_type,
-			'meta_key' => BASE . '_square_catalog_id',
-			'meta_value' => $item['id'],
-			'posts_per_page' => 1
-		]);
-
-		$post_data = [
-			'post_title' => $item['item_data']['name'] ?? '',
-			'post_content' => $item['item_data']['description'] ?? '',
-			'post_type' => $post_type,
-			'post_status' => 'publish'
-		];
-
-		if (!empty($existing)) {
-			$post_data['ID'] = $existing[0]->ID;
-			$post_id = wp_update_post($post_data);
-		} else {
-			$post_id = wp_insert_post($post_data);
-		}
-
-		if (!$post_id || is_wp_error($post_id)) {
-			return false;
-		}
-
-		// Map and save meta fields
-		$this->mapSquareFieldsToWordPress($post_id, $item);
-
-		return $post_id;
-	}
-
-	/**
-	 * Map Square fields to WordPress meta
-	 */
-	private function mapSquareFieldsToWordPress(int $post_id, array $item): void
-	{
-		$meta = Meta::forPost($post_id);
-		$field_map = $this->getFieldMapping(get_post_type($post_id));
-
-		$values_to_save = [];
-
-		// Save Square ID
-		$values_to_save['_square_catalog_id'] = $item['id'];
-		$values_to_save['_square_last_sync'] = current_time('mysql');
-
-		// Handle variations if present
-		if (!empty($item['item_data']['variations'])) {
-			$variations_data = [];
-
-			foreach ($item['item_data']['variations'] as $index => $variation) {
-				$var_data = [
-					'name' => $variation['item_variation_data']['name'] ?? '',
-					'sku' => $variation['item_variation_data']['_square_sku'] ?? '',
-				];
-
-				// Extract price
-				if (!empty($variation['item_variation_data']['price_money'])) {
-					$var_data['price'] = $variation['item_variation_data']['price_money']['amount'] / 100;
-				}
-
-				// Extract other variation fields
-				$variation_map = $this->getVariationMapping(get_post_type($post_id));
-				foreach ($variation_map as $square_field => $wp_field) {
-					if (isset($variation['item_variation_data'][$square_field])) {
-						$var_data[$wp_field] = $variation['item_variation_data'][$square_field];
-					}
-				}
-
-				$variations_data[] = $var_data;
-
-				// Save variation Square ID
-				update_post_meta($post_id, BASE . '_square_variation_' . $index . '_id', $variation['id']);
-			}
-
-			$values_to_save['product_variations'] = $variations_data;
-		}
-
-		// Map other fields
-		foreach ($field_map as $square_field => $wp_field) {
-			if ($square_field !== 'price' && isset($item['item_data'][$square_field])) {
-				$values_to_save[$wp_field] = $item['item_data'][$square_field];
-			}
-		}
-
-		// Save all values at once
-		$meta->setAll($values_to_save);
-	}
-
-	/**
-	 * Process customer sync
-	 */
-	private function processSyncCustomer(array $data): array
-	{
-		$user_id = $data['user_id'] ?? 0;
-
-		if (!$user_id) {
-			return [
-				'success' => false,
-				'result' => ['error' => 'No user ID provided']
-			];
-		}
-
-		$user = get_user_by('ID', $user_id);
-		if (!$user) {
-			return [
-				'success' => false,
-				'result' => ['error' => 'User not found']
-			];
-		}
-
-		// Get or create Square customer
-		$square_customer_id = $this->getOrCreateSquareCustomer([
-			'email' => $user->user_email,
-			'name' => $user->display_name
-		]);
-
-		if ($square_customer_id) {
-			update_user_meta($user_id, BASE . '_square_customer_id', $square_customer_id);
-
-			return [
-				'success' => true,
-				'result' => ['customer_id' => $square_customer_id]
-			];
-		}
-
-		return [
-			'success' => false,
-			'result' => ['error' => 'Could not sync customer']
-		];
-	}
-
-	/**
-	 * Handle catalog webhook
-	 */
-	private function handleCatalogWebhook(array $data): bool
-	{
-		// Queue sync from Square for updated items
-		$this->queueOperation('import_catalog', [
-			'types' => ['ITEM'],
-			'updated_at' => $data['object']['catalog_version']['updated_at'] ?? null
-		], [
-			'delay' => 60 // Wait a minute to batch multiple updates
-		]);
-
-		return true;
-	}
-
-	/**
-	 * Handle customer webhook
-	 */
-	private function handleCustomerWebhook(array $data): bool
-	{
-		$square_customer_id = $data['object']['customer']['id'] ?? '';
-		$email = $data['object']['customer']['email_address'] ?? '';
-
-		if (!$square_customer_id || !$email) {
-			return false;
-		}
-
-		// Find WordPress user with this Square customer ID
-		$users = get_users([
-			'meta_key' => BASE . '_square_customer_id',
-			'meta_value' => $square_customer_id,
-			'number' => 1
-		]);
-
-		if (!empty($users)) {
-			// Update user meta with latest Square data
-			$user = $users[0];
-			update_user_meta($user->ID, BASE . '_square_customer_updated', current_time('mysql'));
-
-			// Clear cached customer data
-			$this->cache->forget('square_customer_' . $user->ID);
-		}
-
-		return true;
-	}
-
-	/**
-	 * Handle payment webhook
-	 */
-	private function handlePaymentWebhook(array $data): bool
-	{
-		$payment_id = $data['object']['payment']['id'] ?? '';
-		$order_id = $data['object']['payment']['order_id'] ?? '';
-		$status = $data['object']['payment']['status'] ?? '';
-
-		if (!$payment_id) {
-			return false;
-		}
-
-		if ($order_id) {
-			$order = $this->getOrderPost($order_id);
-			if ($order) {
-				Meta::forPost($order)->set('square_payment_status', $status);
-			}
-		}
-
-		// Trigger action for other integrations
-		do_action(BASE . 'square_payment_updated', $payment_id, $status, $order_id, $data);
-
-		return true;
-	}
-
-	/**
-	 * Get the appropriate API base URL
-	 */
-	protected function getApiUrl(string $endpoint, ?string $baseKey = null): string
-	{
-		return rtrim($this->apiBase[$this->environment], '/') . '/' . ltrim($endpoint, '/');
-	}
-
-	/**
-	 * Handle importing catalog from Square
-	 */
-	protected function handleImportFromSquare(): array
-	{
-		if (!$this->locationId) {
-			return [
-				'success' => false,
-				'message' => 'Please select a location first'
-			];
-		}
-
-		$this->queueOperation('import_catalog', [
-			'types' => ['ITEM'],
-			'user_id' => $this->userID
-		], [
-			'priority' => 'normal'
-		]);
-
-		return [
-			'success' => true,
-			'message' => 'Import from Square queued'
-		];
-	}
-	/**
-	 * Handle syncing to Square
-	 */
-	protected function handleSyncToSquare(): array
-	{
-		if (!$this->locationId) {
-			return [
-				'success' => false,
-				'message' => 'Please select a location first'
-			];
-		}
-
-		$post_types = array_map(function($type) {
-			return jvbCheckBase($type);
-		}, $this->syncPostTypes);
-
-		// Get all posts to sync
-		$posts = get_posts([
-			'post_type' => $post_types,
-			'posts_per_page' => -1,
-			'post_status' => 'publish'
-		]);
-
-		$post_ids = wp_list_pluck($posts, 'ID');
-
-		// Queue sync operation
-		$this->queueOperation('sync_to_square', [
-			'items' => $post_ids,
-			'user_id' => $this->userID
-		], [
-			'priority' => 'normal'
-		]);
-
-		return [
-			'success' => true,
-			'message' => sprintf('Queued %d items for sync to Square', count($post_ids))
-		];
-	}
-	/**
-	 * Refresh category mappings from Square
-	 */
-	protected function handleRefreshCategories(): array
-	{
-		// Refresh category mappings from Square
-		$response = $this->getRequest('catalog/list?types=CATEGORY');
-
-		if (!is_wp_error($response) && isset($response['objects'])) {
-			$count = 0;
-			foreach ($response['objects'] as $category) {
-				if ($category['type'] === 'CATEGORY') {
-					$name = $category['category_data']['name'] ?? '';
-					if ($name) {
-						update_option(
-							BASE . 'square_category_' . sanitize_title($name),
-							$category['id']
-						);
-						$count++;
-					}
-				}
-			}
-			return [
-				'success' => true,
-				'message' => sprintf('Refreshed %d categories', $count),
-				'count' => $count
-			];
-		} else {
-			return [
-				'success' => false,
-				'message' => 'Failed to refresh categories'
-			];
-		}
-	}
-	/**
-	 * Get available Square locations
-	 */
-	protected function handleGetLocations(): array
-	{
-		// Make sure we have an access token
-		if (empty($this->access_token)) {
-			return [
-				'success' => false,
-				'message' => 'Access token required to fetch locations'
-			];
-		}
-
-		// Fetch available Square locations
-		$response = $this->getRequest('locations');
-
-		if (!is_wp_error($response) && isset($response['locations'])) {
-			$locations = array_map(function($loc) {
-				return [
-					'id' => $loc['id'],
-					'name' => $loc['name'] ?? 'Unnamed Location',
-					'address' => $loc['address']['address_line_1'] ?? '',
-					'status' => $loc['status'] ?? 'ACTIVE'
-				];
-			}, $response['locations']);
-
-			// Filter to only active locations
-			$active_locations = array_filter($locations, function($loc) {
-				return $loc['status'] === 'ACTIVE';
-			});
-
-			// Format for select field
-			if (isset($_REQUEST['for_select'])) {
-				$options = [];
-				foreach ($active_locations as $location) {
-					$label = $location['name'];
-					if ($location['address']) {
-						$label .= ' - ' . $location['address'];
-					}
-					$options[$location['id']] = $label;
-				}
-
-				return [
-					'success' => true,
-					'options' => $options
-				];
-			}
-
-			return [
-				'success' => true,
-				'locations' => array_values($active_locations)
-			];
-		}
-
-		return [
-			'success' => false,
-			'message' => 'Failed to fetch locations. Please check your access token and environment settings.'
-		];
-	}
-
-	protected function validateCredentials(array $credentials): bool
-	{
-		// For OAuth services, we need different validation based on setup stage
-		if ($this->isOAuthService) {
-			// Stage 1: Initial setup (need app credentials)
-			$hasAppCredentials = !empty($credentials['client_id'])
-				&& !empty($credentials['client_secret']);
-
-			if (!$hasAppCredentials) {
-				$this->logError('Missing required Square application credentials', [
-					'has_app_id' => !empty($credentials['client_id']),
-					'has_app_secret' => !empty($credentials['client_secret'])
-				]);
-				return false;
-			}
-
-			// Stage 2: After OAuth (should have access token)
-			// Access token might not exist yet if we're just saving app credentials
-			// before OAuth flow, so we only validate format if it exists
-			if (!empty($credentials['access_token'])) {
-				// Validate token format (Square tokens are long alphanumeric strings)
-				if (!is_string($credentials['access_token']) ||
-					strlen($credentials['access_token']) < 20) {
-					$this->logError('Invalid access token format');
-					return false;
-				}
-
-				// Validate merchant_id if present (should be set after OAuth)
-				if (!empty($credentials['merchant_id'])) {
-					if (!is_string($credentials['merchant_id']) ||
-						strlen($credentials['merchant_id']) < 10) {
-						$this->logError('Invalid merchant ID format');
-						return false;
-					}
-				}
-			}
-
-			// Validate environment setting
-			if (isset($credentials['environment'])) {
-				$validEnvironments = ['sandbox', 'production'];
-				if (!in_array($credentials['environment'], $validEnvironments)) {
-					$this->logError('Invalid environment setting', [
-						'provided' => $credentials['environment'],
-						'valid' => $validEnvironments
-					]);
-					return false;
-				}
-			}
-
-			return true;
-		}
-
-		// Fallback for non-OAuth (shouldn't happen for Square, but good practice)
-		return !empty($credentials);
-	}
-
-
-	protected function sanitizeCredentials(array $credentials): array
-	{
-		$sanitized = [];
-
-		// Define which fields should be sanitized and how
-		$text_fields = [
-			'client_id',
-			'client_secret',
-			'access_token',
-			'refresh_token',
-			'merchant_id',
-			'location_id',
-			'webhook_signature_key'
-		];
-
-		foreach ($credentials as $key => $value) {
-			// Handle text fields - trim whitespace but preserve case
-			if (in_array($key, $text_fields)) {
-				if (is_string($value)) {
-					$sanitized[$key] = trim($value);
-				} else {
-					$sanitized[$key] = $value;
-				}
-			}
-			// Handle environment field
-			elseif ($key === 'environment') {
-				$sanitized[$key] = sanitize_key($value);
-			}
-			// Handle boolean fields
-			elseif ($key === 'use_sandbox' || str_starts_with($key, 'enable_')) {
-				$sanitized[$key] = (bool) $value;
-			}
-			// Handle numeric fields
-			elseif ($key === 'expires_at' || $key === 'updated_at' || $key === 'expires_in') {
-				$sanitized[$key] = is_numeric($value) ? (int) $value : $value;
-			}
-			// Handle arrays (like locations)
-			elseif (is_array($value)) {
-				$sanitized[$key] = array_map('sanitize_text_field', $value);
-			}
-			// Default sanitization for other string fields
-			else {
-				if (is_string($value)) {
-					$sanitized[$key] = sanitize_text_field($value);
-				} else {
-					$sanitized[$key] = $value;
-				}
-			}
-		}
-
-		return $sanitized;
-	}
-
-	public function getServiceDescription(): string
-	{
-		return "Connect with Square for payment processing, inventory management, and customer synchronization.";
-	}
-
-	public function setContentTypes(): void
-	{
-		$this->has_content = true;
-		$this->defaultContent = 'REGULAR';
-		$base = $this->setBaseFields();
-		$this->contentTypes = [
-			'REGULAR'	=> $base,
-			'FOOD_AND_BEV'	=> array_merge($base, $this->setFoodAndBevFields()),
-			'APPOINTMENTS_SERVICE' => array_merge($this->setAppointmentServiceFields()),
-			'EVENT'			=> array_merge($this->setEventFields()),
-			'GIFT_CARD'	=> array_merge($this->setGiftCardFields())
-		];
-	}
-	public function getAdditionalFields(?string $content_type = null):array {
-		if ($content_type === 'customer') {
-			return $this->getCustomerFields();
-		}
-		if ($content_type && array_key_exists($content_type, $this->contentTypes)){
-			$array = $this->contentTypes[$content_type];
-			return array_combine(
-				array_map(fn($k) => '_square_' . $k, array_keys($array)),
-				$array
-			);
-		} else if ($content_type && !array_key_exists($content_type, $this->contentTypes)) {
-			error_log('Could not get default fields for '.$this->service_name.' content type: '.$content_type);
-			return [];
-		}
-		$array = $this->setBaseFields();
-        return array_combine(
-            array_map(fn($k) => '_square_' . $k, array_keys($array)),
-            $array
-        );
-	}
-
-	protected function getCustomerFields():array
-	{
-		return [
-			'customer_id'	=> [
-				'type'	=> 'text',
-				'label'	=> 'Square Customer ID',
-				'hidden'=> true,
-				'section'=> 'your-account'
-			],
-			'address_line_1'	=> [
-				'type'	=> 'text',
-				'label'	=> 'Address Line 1',
-				'hint'	=> 'ex: 6551 111 St NW',
-				'required'	=> true,
-				'section'	=> 'address'
-			],
-			'address_line_2'	=> [
-				'type'	=> 'text',
-				'label'	=> 'Address Line 2',
-				'hint'	=> 'ex: Unit 2',
-				'section'	=> 'address'
-			],
-			'city'	=> [
-				'type'	=> 'text',
-				'label'=> 'City',
-				'section'	=> 'address',
-				'required'	=> true,
-			],
-			'state' => [
-				'type'	=> 'text',
-				'label' => 'Province',
-				'hint' => 'The two-character code, example: AB',
-				'default'=> 'AB',
-				'section'	=> 'address',
-				'required'	=> true,
-			],
-			'zip_code' => [
-				'type'	=> 'text',
-				'label'	=> 'Postal Code',
-				'section'=> 'address'
-			],
-			'countryCode'	=> [
-				'type'	=> 'text',
-				'label'	=> 'Country Code',
-				'hint'	=> 'The tw-character country code, example: CA',
-				'default'	=> 'CA',
-				'section'	=> 'address',
-				'required'	=> true,
-			]
-		];
-	}
-	protected function setBaseFields():array
-	{
-		return [
-			'price' => [
-				'type'        => 'number',
-				'label'       => 'Price',
-				'step'        => 0.01,
-				'max'         => 99999,
-				'description' => 'Price for this variation'
-			],
-			'sku' => [
-				'type'        => 'text',
-				'label'       => 'SKU',
-				'description' => 'Stock keeping unit'
-			],
-			'cart_quantity'	=> [
-				'type'	=> 'number',
-				'label'	=> 'Quantity',
-				'hidden'	=> true,
-			],
-			'available_for_pickup' => [
-				'type'	=> 'true_false',
-				'label'	=> 'Available for Pick Up',
-				'section'=> 'square-advanced'
-			],
-			'available_online' => [
-				'type'	=> 'true_false',
-				'label'	=> 'Available online',
-				'section'=> 'square-advanced'
-			],
-			'product_variations' => [
-				'type'        => 'repeater',
-				'label'       => 'Product Variations',
-				'description' => 'Different versions of this product (sizes, colors, etc.)',
-				'add_label'   => 'Add Variation',
-				'section'     => 'variations',
-				'fields'      => $this->setVariationFields()
-			],
-		];
-	}
-
-	protected function setVariationFields(?string $type = null):array
-	{
-		$fields = [
-			'name' => [
-				'type'        => 'text',
-				'label'       => 'Variation Name',
-				'description' => 'e.g., "Small", "Large", "Red", etc.'
-			],
-            'sku' => [
-                'type'        => 'text',
-                'label'       => 'SKU',
-                'description' => 'Stock keeping unit'
-            ],
-			'price' => [
-				'type'        => 'number',
-				'label'       => 'Price',
-				'step'        => 0.01,
-				'max'         => 99999,
-				'description' => 'Price for this variation'
-			],
-			'track_inventory'	=> [
-				'type'	=> 'true_false',
-				'label'	=> 'Track Inventory',
-			],
-			'item_id' => [
-				'type'        => 'text',
-				'label'       => 'Square Variation ID',
-				'description' => 'Square catalog ID for this variation',
-				'hidden'      => true
-			],
-			'last_sync' => [
-				'type'   => 'datetime',
-				'label'  => 'Last Sync',
-				'hidden' => true
-			]
-		];
-
-		switch ($type) {
-			case 'FOOD_AND_BEV':
-				$fields['ingredients']	= [
-					'type'	=> 'textarea',
-					'label'	=> 'Ingredients List',
-					'description'	=> 'Separate ingredients with commas',
-				];
-				$fields['preparation_time_duration']	= [
-					'type'	=> 'number',
-					'label'	=> 'Preparation time (in minutes)',
-				];
-				break;
-			case 'GIFT_CARD':
-				$fields['gift_card_type']	= [
-					'type'	=> 'select',
-					'label'	=> 'Gift Card Type',
-					'options'	=> [
-						'PHYSICAL'	=> 'Physical',
-						'DIGITAL'	=> 'Digital',
-					],
-					'default'	=> 'DIGITAL',
-				];
-				break;
-			case 'APPOINTMENTS_SERVICE':
-				$fields['service_duration']	= [
-					'type'	=> 'number',
-					'label'	=> 'Duration of Service in Minutes'
-				];
-				$fields['available_for_booking']	= [
-					'type'	=> 'true_false',
-					'label'	=> 'Available for Booking'
-				];
-				break;
-		}
-		return array_combine(
-            array_map(fn($k) => '_square_' . $k, array_keys($fields)),
-            $fields
-        );
-	}
-	protected function setFoodAndBevFields():array
-	{
-		return [
-			'ingredients'	=> [
-				'type'	=> 'textarea',
-				'label'	=> 'Ingredients',
-				'hint'	=> 'A comma separated list of ingredients'
-			],
-			'preparation_time_duration' => [
-				'label'	=> 'Preparation Time',
-				'type'	=> 'number',
-				'hint'	=> 'Preparation time in minutes'
-			],
-			'product_variations' => [
-				'type'        => 'repeater',
-				'label'       => 'Product Variations',
-				'description' => 'Different versions of this product (sizes, colors, etc.)',
-				'add_label'   => 'Add Variation',
-				'section'     => 'variations',
-				'fields'      => $this->setVariationFields('FOOD_AND_BEV')
-			],
-		];
-		/* TODO: Set definitions for:
-			'dietary_preferences'	=> [
-
-			],
-			'calories_text'	=> [
-
-			],
-		*/
-	}
-	protected function setAppointmentServiceFields():array
-	{
-		return [
-
-			'product_variations' => [
-				'type'        => 'repeater',
-				'label'       => 'Product Variations',
-				'description' => 'Different versions of this product (sizes, colors, etc.)',
-				'add_label'   => 'Add Variation',
-				'section'     => 'variations',
-				'fields'      => $this->setVariationFields('APPOINTMENTS_SERVICE')
-			],
-		];
-	}
-	protected function setEventFields():array
-	{
-		return [
-			'venue_details'	=> [
-				'type'	=> 'textarea',
-				'label'	=> 'Venue Details',
-			],
-			'capacity'	=> [
-				'type'	=> 'number',
-				'label'	=> 'Capacity'
-			],
-			'product_variations' => [
-				'type'        => 'repeater',
-				'label'       => 'Product Variations',
-				'description' => 'Different versions of this product (sizes, colors, etc.)',
-				'add_label'   => 'Add Variation',
-				'section'     => 'variations',
-				'fields'      => $this->setVariationFields('EVENT')
-			],
-		];
-	}
-	protected function setGiftCardFields():array
-	{
-		return [
-//			'allowed_locations'	=> [
-//
-//			],
-			'product_variations' => [
-				'type'        => 'repeater',
-				'label'       => 'Product Variations',
-				'description' => 'Different versions of this product (sizes, colors, etc.)',
-				'add_label'   => 'Add Variation',
-				'section'     => 'variations',
-				'fields'      => $this->setVariationFields('GIFT_CARD')
-			],
-		];
-	}
-
-	/*********************************************************
-	IMAGE PROCESSING
-	 *********************************************************/
-	/**
-	 * Upload image file to Square
-	 *
-	 * @param int $imgID WordPress attachment ID
-	 * @return array|WP_Error Upload result or error
-	 */
-	protected function uploadImageToSquare(int $imgID): array|WP_Error
-	{
-		$supported_image_id = $this->getSupportedImage($imgID);
-
-		// Check if already uploaded
-		$existing_square_image_id = $this->getSquareImageId($supported_image_id);
-		if ($existing_square_image_id) {
-			return [
-				'success' => true,
-				'image_id' => $existing_square_image_id
-			];
-		}
-
-		$file_path = get_attached_file($supported_image_id);
-		if (!file_exists($file_path)) {
-			return new WP_Error('file_not_found', 'Image file not found');
-		}
-
-		// Verify file type
-		$mime_type = get_post_mime_type($supported_image_id);
-		if (!in_array($mime_type, ['image/jpeg', 'image/png', 'image/gif'])) {
-			return new WP_Error('invalid_type', 'Square only supports JPEG, PNG, and GIF images');
-		}
-
-		$image_title = get_the_title($supported_image_id);
-		$alt_text = get_post_meta($supported_image_id, '_wp_attachment_image_alt', true);
-
-		// Build multipart request - SINGLE STEP
-		$boundary = wp_generate_password(24);
-		$headers = $this->getRequestHeaders();
-		$headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
-
-		// Request JSON part
-		$request_json = [
-			'idempotency_key' => wp_generate_uuid4(),
-			'image' => [
-				'type' => 'IMAGE',
-				'id' => '#IMAGE_' . $supported_image_id . '_' . time(),
-				'image_data' => [
-					'name' => $image_title ?: 'Image',
-					'caption' => $alt_text ?: ''
-				]
-			],
-			'object_id'	=> $supported_image_id
-		];
-
-		$body = $this->buildMultipartBody($file_path, $request_json, $boundary);
-
-		$response = wp_remote_post(
-			$this->getApiUrl('v2/catalog/images'),
-			[
-				'headers' => $headers,
-				'body' => $body,
-				'timeout' => 60
-			]
-		);
-
-		if (is_wp_error($response)) {
-			return $response;
-		}
-
-		$data = json_decode(wp_remote_retrieve_body($response), true);
-
-		if (!empty($data['errors'])) {
-			return new WP_Error('upload_error', $data['errors'][0]['detail'] ?? 'Unknown error');
-		}
-
-		if (!empty($data['image']['id'])) {
-			$this->setSquareImageId($supported_image_id, $data['image']['id']);
-			return [
-				'success' => true,
-				'image_id' => $data['image']['id']
-			];
-		}
-
-		return new WP_Error('upload_failed', 'Failed to upload image');
-	}
-
-	protected function buildMultipartBody(string $file_path, array $request_json, string $boundary): string
-	{
-		$eol = "\r\n";
-		$body = '';
-
-		// Add request JSON part
-		$body .= '--' . $boundary . $eol;
-		$body .= 'Content-Disposition: form-data; name="request"' . $eol;
-		$body .= 'Content-Type: application/json' . $eol . $eol;
-		$body .= json_encode($request_json) . $eol;
-
-		// Add image file part
-		$filename = basename($file_path);
-		$file_contents = file_get_contents($file_path);
-		$mime_type = mime_content_type($file_path);
-
-		$body .= '--' . $boundary . $eol;
-		$body .= 'Content-Disposition: form-data; name="file"; filename="' . $filename . '"' . $eol;
-		$body .= 'Content-Type: ' . $mime_type . $eol . $eol;
-		$body .= $file_contents . $eol;
-		$body .= '--' . $boundary . '--' . $eol;
-
-		return $body;
-	}
-
-	/**
-	 * Attach image to catalog item
-	 *
-	 * @param int $postID WordPress post ID
-	 * @param string $square_image_id Square image ID
-	 * @return array|WP_Error Result or error
-	 */
-	public function attachImageToCatalogItem(int $postID, string $square_image_id): array|WP_Error
-	{
-		$square_catalog_id = get_post_meta($postID, BASE . '_square_catalog_id', true);
-
-		if (!$square_catalog_id) {
-			return new WP_Error('no_catalog_id', 'Post has not been synced to Square');
-		}
-
-		// Get the current catalog item
-		$response = $this->getRequest('catalog/object/' . $square_catalog_id);
-
-		if (is_wp_error($response)) {
-			return $response;
-		}
-
-		if (empty($response['object'])) {
-			return new WP_Error('item_not_found', 'Catalog item not found in Square');
-		}
-
-		$catalog_object = $response['object'];
-
-		// Add image to item data
-		$catalog_object['item_data']['image_ids'] = [$square_image_id];
-
-		// Update the catalog item
-		$update_response = $this->postRequest('catalog/batch-upsert', [
-			'idempotency_key' => wp_generate_uuid4(),
-			'batches' => [[
-				'objects' => [$catalog_object]
-			]]
-		]);
-
-		if (is_wp_error($update_response)) {
-			return $update_response;
-		}
-
-		return [
-			'success' => true,
-			'message' => 'Image attached to catalog item'
-		];
-	}
-
-	/**
-	 * Get stored Square image ID for an attachment
-	 *
-	 * @param int $attachment_id WordPress attachment ID
-	 * @return string|false Square image ID or false if not found
-	 */
-	public function getSquareImageId(int $attachment_id): string|false
-	{
-		$image_id = get_post_meta($attachment_id, BASE . 'square_image_id', true);
-
-		if ($image_id) {
-			// Verify it still exists in Square
-			return $this->verifySquareImage($image_id) ? $image_id : false;
-		}
-
-		return false;
-	}
-
-	/**
-	 * Store Square image ID for an attachment
-	 *
-	 * @param int $attachment_id WordPress attachment ID
-	 * @param string $square_image_id Square image ID
-	 * @return bool Success
-	 */
-	public function setSquareImageId(int $attachment_id, string $square_image_id): bool
-	{
-		return update_post_meta($attachment_id, BASE . 'square_image_id', $square_image_id);
-	}
-
-	/**
-	 * Verify image exists in Square catalog
-	 *
-	 * @param string $square_image_id Square image ID
-	 * @return bool Whether image exists
-	 */
-	protected function verifySquareImage(string $square_image_id): bool
-	{
-		$response = $this->getRequest('catalog/object/' . $square_image_id);
-
-		if (is_wp_error($response)) {
-			return false;
-		}
-
-		return !empty($response['object']) && $response['object']['type'] === 'IMAGE';
-	}
-
-	/**
-	 * Batch upload images for multiple posts
-	 *
-	 * @param array $post_ids Array of WordPress post IDs
-	 * @return array Results for each post
-	 */
-	private function batchUploadImages(array $post_ids): array
-	{
-		$image_mappings = [];
-		$image_objects = [];
-
-		// First pass: collect images that need uploading
-		foreach ($post_ids as $post_id) {
-			$thumbnail_id = get_post_thumbnail_id($post_id);
-			if (!$thumbnail_id) {
-				$image_mappings[$post_id] = null;
-				continue;
-			}
-
-			// Check if already uploaded and still exists in Square
-			$existing_square_id = get_post_meta($thumbnail_id, BASE . '_square_image_id', true);
-			if ($existing_square_id && $this->verifySquareImage($existing_square_id)) {
-				$image_mappings[$post_id] = $existing_square_id;
-				continue;
-			}
-
-			// Get supported image format (handles WebP conversion)
-			$supported_image_id = $this->getSupportedImage($thumbnail_id);
-			if (!$supported_image_id) {
-				$image_mappings[$post_id] = null;
-				continue;
-			}
-
-			// Upload the image to Square using the proper endpoint
-			$square_image_id = $this->uploadSingleImageToSquare($supported_image_id, $post_id);
-
-			if ($square_image_id && !is_wp_error($square_image_id)) {
-				$image_mappings[$post_id] = $square_image_id;
-				// Store the Square image ID for future reference
-				update_post_meta($thumbnail_id, BASE . '_square_image_id', $square_image_id);
-			} else {
-				$this->logError('Failed to upload image for post', [
-					'post_id' => $post_id,
-					'thumbnail_id' => $thumbnail_id,
-					'error' => is_wp_error($square_image_id) ? $square_image_id->get_error_message() : 'Unknown error'
-				]);
-				$image_mappings[$post_id] = null;
-			}
-		}
-
-		return $image_mappings;
-	}
-
-	/**
-	 * Upload a single image to Square using the CreateCatalogImage endpoint
-	 *
-	 * @param int $attachment_id WordPress attachment ID
-	 * @param int $post_id Associated post ID
-	 * @return string|WP_Error Square image ID or error
-	 */
-	private function uploadSingleImageToSquare(int $attachment_id, int $post_id): string|WP_Error
-	{
-		$file_path = get_attached_file($attachment_id);
-
-		if (!file_exists($file_path)) {
-			return new WP_Error('file_not_found', 'Image file not found');
-		}
-
-		// Verify file type
-		$mime_type = get_post_mime_type($attachment_id);
-		$allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
-
-		if (!in_array($mime_type, $allowed_types)) {
-			return new WP_Error('invalid_type', 'Square only supports JPEG, PNG, and GIF images');
-		}
-
-		// Prepare multipart form data
-		$boundary = wp_generate_password(24);
-		$headers = $this->getRequestHeaders();
-		$headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
-
-		// Build the request body
-		$eol = "\r\n";
-		$body = '';
-
-		// Add request JSON
-		$request_data = [
-			'idempotency_key' => wp_generate_uuid4(),
-			'image' => [
-				'type' => 'IMAGE',
-				'id' => '#TEMP_IMAGE_' . $attachment_id . '_' . time(),
-				'image_data' => [
-					'name' => get_the_title($attachment_id) ?: basename($file_path),
-					'caption' => get_post_meta($attachment_id, '_wp_attachment_image_alt', true) ?:
-						sprintf('Image for %s', get_the_title($post_id))
-				]
-			]
-		];
-
-		$body .= '--' . $boundary . $eol;
-		$body .= 'Content-Disposition: form-data; name="request"' . $eol;
-		$body .= 'Content-Type: application/json' . $eol . $eol;
-		$body .= json_encode($request_data) . $eol;
-
-		// Add image file
-		$filename = basename($file_path);
-		$file_contents = file_get_contents($file_path);
-
-		$body .= '--' . $boundary . $eol;
-		$body .= 'Content-Disposition: form-data; name="image"; filename="' . $filename . '"' . $eol;
-		$body .= 'Content-Type: ' . $mime_type . $eol . $eol;
-		$body .= $file_contents . $eol;
-
-		// End boundary
-		$body .= '--' . $boundary . '--' . $eol;
-
-		// Make the request to the CreateCatalogImage endpoint
-		$response = wp_remote_post(
-			$this->getApiUrl('catalog/images'),
-			[
-				'headers' => $headers,
-				'body' => $body,
-				'timeout' => 60
-			]
-		);
-
-		if (is_wp_error($response)) {
-			return $response;
-		}
-
-		$response_body = wp_remote_retrieve_body($response);
-		$data = json_decode($response_body, true);
-
-		if (!empty($data['errors'])) {
-			$error_message = $data['errors'][0]['detail'] ?? 'Unknown error';
-			return new WP_Error('upload_error', $error_message);
-		}
-
-		if (!empty($data['image']['id'])) {
-			$this->logDebug('Successfully uploaded image to Square', [
-				'post_id' => $post_id,
-				'attachment_id' => $attachment_id,
-				'square_image_id' => $data['image']['id']
-			]);
-			return $data['image']['id'];
-		}
-
-		return new WP_Error('upload_failed', 'Failed to get image ID from Square response');
-	}
-
-	protected function handleApiError(int $code, string $body, string $endpoint): void
-	{
-		parent::handleApiError($code, $body, $endpoint);
-
-		$decoded = json_decode($body, true);
-		$errorCode = $decoded['errors'][0]['code'] ?? '';
-
-		// Handle Square-specific OAuth errors
-		if ($errorCode === 'ACCESS_TOKEN_EXPIRED') {
-			$this->logDebug('Access token expired, attempting refresh');
-			if ($this->refreshOAuthToken()) {
-				// Token refreshed, could retry the request
-				$this->logDebug('Token refreshed successfully');
-			}
-		} elseif ($errorCode === 'UNAUTHORIZED' || $errorCode === 'ACCESS_TOKEN_REVOKED') {
-			$this->logError('Square authorization revoked or invalid', [
-				'error_code' => $errorCode,
-				'user_id' => $this->userID
-			]);
-			// Clear invalid credentials
-			$this->deleteCredentials();
-		}
-	}
-
-	public function createSquareOrder(array $items, ?string $customer_id, array $data): array|WP_Error
-	{
-		// Build line items for Square
-		$line_items = [];
-		foreach ($items as $item) {
-			$line_item = [
-				'quantity' => (string)$item['quantity'], // MUST be string!
-			];
-
-			// Use catalog_object_id if available (recommended)
-			if (!empty($item['catalog_object_id'])) {
-				$line_item['catalog_object_id'] = $item['catalog_object_id'];
-				$line_item['catalog_version'] = $item['catalog_version'] ?? null;
-			} else {
-				// Ad-hoc line item (not recommended - no tax/inventory automation)
-				$line_item['name'] = $item['name'];
-				$line_item['base_price_money'] = [
-					'amount' => (int)$item['_square_price'],
-					'currency' => $this->getCurrency()
-				];
-			}
-
-			if (!empty($item['note'])) {
-				$line_item['note'] = $item['note'];
-			}
-
-			$line_items[] = $line_item;
-		}
-
-		$order_data = [
-			'idempotency_key' => wp_generate_uuid4(), // Different from payment idempotency key
-			'order' => [
-				'location_id' => $this->locationId,
-				'line_items' => $line_items,
-				'state' => 'OPEN'
-			]
-		];
-
-		// Add customer if available
-		if ($customer_id) {
-			$order_data['order']['customer_id'] = $customer_id;
-		}
-
-		// Add metadata
-		if (!empty($data['note'])) {
-			$order_data['order']['metadata'] = [
-				'special_instructions' => $data['note']
-			];
-		}
-
-		if (!empty($data['pickup_time'])) {
-			$order_data['order']['metadata']['pickup_time'] = $data['pickup_time'];
-		}
-
-		return $this->postRequest('orders', $order_data);
-	}
-
-	public function createSquarePayment(
-		string $source_id,
-		string $idempotency_key,
-		int $amount_cents,
-		string $order_id,
-		?string $customer_id
-	): array|WP_Error
-	{
-		$payment_data = [
-			'idempotency_key' => $idempotency_key,
-			'source_id' => $source_id,
-			'amount_money' => [
-				'amount' => $amount_cents, // Already in cents!
-				'currency' => $this->getCurrency()
-			],
-			'order_id' => $order_id,
-			'location_id' => $this->locationId,
-			'autocomplete' => true, // Capture immediately
-		];
-
-		// Add customer if available
-		if ($customer_id) {
-			$payment_data['customer_id'] = $customer_id;
-		}
-
-		// Add reference ID for tracking
-		$payment_data['reference_id'] = 'WP_' . time();
-
-		return $this->postRequest('payments', $payment_data);
-	}
-
-	public function saveOrderToWordPress(array $order_data): int
-	{
-		// Extract customer info
-		$customer_email = $order_data['customer']['email'] ?? '';
-		$customer_name = $order_data['customer']['name'] ?? '';
-
-		// Find or create WP user for logged-in association
-		$user_id = 0;
-		if ($customer_email) {
-			$user = get_user_by('email', $customer_email);
-			if ($user) {
-				$user_id = $user->ID;
-				// Store Square customer ID on user
-				if (!empty($order_data['square_customer_id'])) {
-					update_user_meta($user_id, BASE . '_square_customer_id', $order_data['square_customer_id']);
-				}
-			}
-		}
-
-		// Create order post
-		$order_post_id = wp_insert_post([
-			'post_type' => BASE.$this->orderPostType,
-			'post_title' => 'Order #' . $order_data['square_order_id'],
-			'post_status' => 'publish',
-			'post_author' => $user_id // Associate with user if logged in
-		]);
-
-		if (!$order_post_id || is_wp_error($order_post_id)) {
-			$this->logError('Failed to create order post', ['order_data' => $order_data]);
-			return 0;
-		}
-
-		// Save all order meta
-		$meta = Meta::forPost($order_post_id);
-		$fields = $this->getOrderFields();
-		unset($fields['post_title']);
-
-		$meta->setAll([
-			'square_order_id' => $order_data['square_order_id'],
-			'square_payment_id' => $order_data['square_payment_id'] ?? '',
-			'square_customer_id' => $order_data['square_customer_id'] ?? '',
-			'amount' => $order_data['amount'],
-			'status' => $order_data['status'],
-			'fulfillment_status' => $order_data['fulfillment_status'] ?? 'PROPOSED',
-			'pickup_time' => $order_data['pickup_time'] ?? '',
-			'customer_email' => $customer_email,
-			'customer_name' => $customer_name,
-			'customer_phone' => $order_data['customer']['phone'] ?? '',
-			'special_instructions' => $order_data['note'] ?? '',
-			'items' => $order_data['items'],
-			'receipt_url' => $order_data['receipt_url'] ?? '',
-			'created_at' => current_time('mysql'),
-			'updated_at' => current_time('mysql')
-		]);
-
-		return $order_post_id;
-	}
-
-	/**
-	 * Get currency code
-	 */
-	private function getCurrency(): string
-	{
-		return get_option(BASE . 'currency', 'CAD');
-	}
-
-	/**
-	 * Get customer with saved cards (2025-compliant)
-	 */
-	public function getUserCards(string $customer_id): array
-	{
-		$response = $this->getRequest('cards?customer_id=' . $customer_id);
-		return $response['cards'] ?? [];
-	}
-
-
-	public function getUserOrders(string $customer_email): array
-	{
-		// First get Square customer ID
-		$customer_response = $this->postRequest('customers/search', [
-			'filter' => [
-				'email_address' => ['exact' => $customer_email]
-			]
-		]);
-
-		$customer_id = $customer_response['customers'][0]['id'] ?? null;
-		if (!$customer_id) {
-			return [];
-		}
-
-		// Get their orders
-		$orders_response = $this->postRequest('orders/search', [
-			'filter' => [
-				'customer_filter' => [
-					'customer_ids' => [$customer_id]
-				]
-			],
-			'sort' => [
-				'sort_field' => 'CREATED_AT',
-				'sort_order' => 'DESC'
-			],
-			'limit' => 50
-		]);
-
-		return $orders_response['orders'] ?? [];
-	}
-
-	public function checkOrderStatus(string $order_id): ?string
-	{
-		// Fetch from Square
-		$response = $this->getRequest('orders/' . $order_id);
-		if (!is_wp_error($response)) {
-			return $response['order']['state'] ?? null;
-		}
-
-		return null;
-	}
-
-	/**
-	 * Single-item sync. Called by IntegrationExecutor::processSyncTo().
-	 * Delegates to syncBatchToService since Square uses batch-upsert.
-	 */
-	public function syncPostToService(int $postID): array|WP_Error
-	{
-		return $this->syncBatchToService(['items' => [$postID]]);
-	}
-
-	/**
-	 * Batch sync — preferred by IntegrationExecutor when available.
-	 * Wraps existing processSyncToSquare which already handles batches.
-	 */
-	public function syncBatchToService(array $data): array|WP_Error
-	{
-		$result = $this->processSyncToSquare($data);
-
-		if (empty($result['success'])) {
-			$errors = implode(', ', $result['result']['errors'] ?? ['Sync failed']);
-			return new WP_Error('square_sync_failed', $errors);
-		}
-
-		return $result;
-	}
-
-	/**
-	 * Delete catalog object from Square.
-	 * Called by IntegrationExecutor::processDeleteFrom().
-	 */
-	public function deleteFromService(string $externalId): array|WP_Error
-	{
-		$result = $this->processDeleteFromSquare(['square_ids' => [$externalId]]);
-
-		if (empty($result['success'])) {
-			return new WP_Error('square_delete_failed', $result['result']['error'] ?? 'Delete failed');
-		}
-
-		return $result;
-	}
-
-	/**
-	 * Import from Square catalog → WordPress.
-	 * Called by IntegrationExecutor::processImport().
-	 */
-	public function importFromService(array $data): array|WP_Error
-	{
-		$result = $this->processSyncFromSquare($data);
-
-		if (empty($result['success'])) {
-			return new WP_Error('square_import_failed', $result['result']['error'] ?? 'Import failed');
-		}
-
-		return $result;
-	}
-
-	/**
-	 * Sync customer to Square.
-	 * Called by IntegrationExecutor::processSyncCustomer().
-	 */
-	public function syncCustomer(array $data): array|WP_Error
-	{
-		$result = $this->processSyncCustomer($data);
-
-		if (empty($result['success'])) {
-			return new WP_Error('square_customer_sync_failed', $result['result']['error'] ?? 'Customer sync failed');
+			return $this->response(false, 'Failed to fetch locations: '.$e->getMessage());
 		}
-
-		return $result;
-	}
-
-	public function addDashboardPages():void
-	{
-		$page = JVB()->dashboard()->addPage('Your Orders', 'orders', 'receipt');
-		$page->setScripts(['jvb-crud']);
-	}
-	public function renderDashPage():void
-	{
-		$this->determineUser();
-
-		$crud = new CRUDSkeleton();
-		$crud->icon('receipt');
-		$crud->title('Your Orders','Here you can see your past orders, and reorder from there if you\'d like');
-		$crud->content($this->orderPostType, 'Order', 'Orders');
-		$crud->addSearch();
-		$crud->addCapabilities(['view']);
-		$crud->setEmptyState(sprintf(
-			'<div class="empty-state">
-				<h3>%sNothing here%s</h3>
-				<p>It doesn\'t look like you have any orders yet.</p>
-				<p>Head on over to <a href="%s">our menu</a> and make your first order!</p>
-			</div>',
-			jvbDashIcon($crud->getIcon()),
-			jvbDashIcon($crud->getIcon()),
-			get_post_type_archive_link(BASE.'menu_item')
-		));
-
-
-		$crud->render();
-	}
-
-
-	public function getOrderPost(string $order_id):int|false
-	{
-		$posts = new WP_Query([
-			'post_type'			=> BASE.$this->orderPostType,
-			'posts_per_page'	=> 1,
-			'meta_key'			=> BASE.'square_order_id',
-			'meta_value'		=> $order_id,
-			'fields'			=> 'ids',
-		]);
-		wp_reset_postdata();
-		return $posts->have_posts() ? $posts[0] : false;
 	}
-	public function getOrderHistory(int $user_id):array
-	{
-		$posts = new WP_Query([
-			'post_type'			=> BASE.$this->orderPostType,
-			'posts_per_page'	=> 25,
-			'author'			=> $user_id,
-			'orderby'			=> 'date',
-			'order'				=> 'desc',
-			'fields'			=> 'ids',
-		]);
-		wp_reset_postdata();
-		return array_map(function ($post) {
-			$fields = Meta::forPost($post);
-			$fields['wp_order_id'] = $post;
-			return $fields;
-		}, $posts->posts);
 
-	}
 
-	public function formatPrice(string|float|int $priceValue):int
-	{
-		//Convert dollars to cents
-		return intval(floatval($priceValue) * 100);
-	}
 }
diff --git a/inc/integrations/services/SquareOld.php b/inc/integrations/services/SquareOld.php
new file mode 100644
index 0000000..d037f42
--- /dev/null
+++ b/inc/integrations/services/SquareOld.php
@@ -0,0 +1,3506 @@
+<?php
+
+namespace JVBase\integrations\services;
+use Exception;
+use JVBase\integrations\Integrations;
+use JVBase\managers\queue\executors\IntegrationExecutor;
+use JVBase\managers\queue\TypeConfig;
+use JVBase\meta\Form;
+use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
+use JVBase\ui\Checkout;
+use JVBase\ui\CRUDSkeleton;
+use WP_Error;
+use WP_Post;
+use WP_Query;
+use WP_User;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+/**
+ * Square Integration Class
+ *
+ * Handles OAuth 2.0 authentication and API interactions with Square
+ * Uses the latest Square API version with OAuth code flow
+ *
+ * @since 1.0.0
+ */
+class SquareOld extends Integrations
+{
+	protected static string $syncCustomer = 'square_sync_customer';
+	protected array $allowedContent = [
+		'REGULAR',
+		'FOOD_AND_BEV',
+		'APPOINTMENTS_SERVICE',
+		'DIGITAL',
+		'EVENT',
+		'DONATION'
+	];
+	/**
+	 * Square API Configuration
+	 */
+	protected string $service_name = 'square';
+	protected array|string $apiBase = [
+		'production' 	=> 'https://connect.squareup.com',
+		'sandbox'		=> 'https://connect.squareupsandbox.com'
+	];
+
+	protected string $apiVersion = '2025-09-24';
+
+	protected string $environment = 'sandbox';
+
+	private const PASSWORD_RESET_INTERVAL = 3; // Reset password every 3 logins, for customers
+
+	/**
+	 * OAuth Configuration
+	 */
+	protected bool $isOAuthService = true;
+
+	protected string $orderPostType = '_square_order';
+	protected array $newOrder = [];
+	protected array $oauth = [
+		'authorize' => '',
+		'token' => '',
+		'revoke' => '',
+		'scopes' => [
+			'MERCHANT_PROFILE_READ',
+			'MERCHANT_PROFILE_WRITE',
+			'PAYMENTS_WRITE',
+			'PAYMENTS_READ',
+			'CUSTOMERS_WRITE',
+			'CUSTOMERS_READ',
+			'INVENTORY_READ',
+			'INVENTORY_WRITE',
+			'ITEMS_READ',
+			'ITEMS_WRITE',
+			'ORDERS_READ',
+			'ORDERS_WRITE'
+		]
+	];
+
+	/**
+	 * Square-specific properties
+	 */
+	protected string $merchantId = '';
+	protected string $locationId = '';
+	protected array $locations = [];
+
+	protected static SquareOld $instance;
+
+	public static function getInstance():self
+	{
+		if (!isset(self::$instance)) {
+			self::$instance = new self();
+		}
+		return self::$instance;
+	}
+
+	protected function __construct(?int $userID = null)
+	{
+
+		// Display properties
+		$this->title = 'Square';
+		$this->icon = 'square-logo';
+
+		$this->refresh_interval = 7 * DAY_IN_SECONDS;
+
+		$this->newOrder = [
+			'post_type'		=> $this->orderPostType,
+			'post_status' 	=> 'PROPOSED',
+		];
+
+		// Define credential fields
+		$this->fields = [
+			'environment'	=> [
+				'type'	=> 'select',
+				'label'	=> 'Environment',
+				'options'	=> [
+					'sandbox'	=> 'Sandbox',
+					'production'=> 'Production',
+				],
+				'default'	=> 'sandbox',
+			],
+			'client_id' => [
+				'type' => 'text',
+				'label' => 'Application ID',
+				'hint' => 'Found in Square Developer Dashboard',
+				'required' => true,
+			],
+			'client_secret' => [
+				'type' => 'text',
+				'subtype' => 'password',
+				'label' => 'Application Secret',
+				'hint' => 'Found in OAuth section of Square Developer Dashboard',
+				'required' => true,
+			],
+			'location_id' => [
+				'type' => 'select',
+				'label' => 'Default Location',
+				'options' => [],
+				'hint' => 'Select default Square location for transactions',
+			]
+		];
+
+		// Advanced settings
+		$this->advanced = [
+			'webhook_signature_key' => [
+				'type' => 'text',
+				'subtype' => 'password',
+				'label' => 'Webhook Signature Key',
+				'hint' => 'Used to verify webhook authenticity',
+			],
+			'access_token' => [
+				'type' => 'text',
+				'subtype' => 'password',
+				'label' => 'Access Token',
+				'hint' => 'Generated automatically after OAuth authorization',
+				'readonly' => true,
+			],
+			'refresh_token' => [
+				'type' => 'text',
+				'subtype' => 'password',
+				'label' => 'Refresh Token',
+				'hint' => 'Used to refresh expired access tokens',
+				'readonly' => true,
+			],
+			'merchant_id' => [
+				'type' => 'text',
+				'label' => 'Merchant ID',
+				'hint' => 'Square Merchant ID (obtained after authorization)',
+				'readonly' => true,
+			],
+		];
+
+		$this->instructions = [
+			'Go to <a href="https://developer.squareup.com/apps" target="_blank">Square Developer Dashboard</a>',
+			'Create a new application or select an existing one',
+			'Navigate to the OAuth section',
+			'Add this redirect URL: <code>' . esc_html($this->getRedirectUri()) . '</code>',
+			'Copy the Application ID and Application Secret',
+			'Click "Authorize Connection" below to complete OAuth flow'
+		];
+
+
+		$this->canSync = [
+			'create' => true,
+			'update' => true,
+			'delete' => true
+		];
+		$this->supportsWebp = false;
+		$this->hasWebhooks = true;
+
+		parent::__construct();
+
+		// Add Square-specific actions
+		$this->actions = array_merge(
+			$this->actions,
+			[
+				'select_location' => 'handleSelectLocation',
+				'fetch_locations' => 'handleFetchLocations',
+				'verify_webhook' => 'handleVerifyWebhook',
+				'test_payment' => 'handleTestPayment'
+			]
+		);
+
+		$this->buttons = array_merge(
+			$this->buttons,
+			[
+				'import_from_square'	=> 'Import Catalog from Square',
+				'sync_to_square' => 'Sync Site to Square',
+			]
+		);
+
+		add_action('init', [$this, 'registerSquarePostTypes'], 5);
+		add_action('init', [$this, 'addDashboardPages'], 10);
+		add_action(BASE.'dashboard_page_orders', [$this, 'renderDashPage']);
+	}
+
+	/**
+	 * Initialize the integration with loaded credentials
+	 */
+	protected function initialize(): void
+	{
+		if (empty($this->credentials)) {
+			$this->loadCredentials();
+		}
+
+		$this->merchantId = $this->credentials['merchant_id'] ?? '';
+		$this->locationId = $this->credentials['location_id'] ?? '';
+		$this->environment = $this->credentials['environment'] ?? 'sandbox';
+
+		// Set OAuth URLs based on environment
+		$baseUrl = ($this->environment === 'production')
+			? 'https://connect.squareup.com'
+			: 'https://connect.squareupsandbox.com';
+
+		$this->oauth['authorize'] = $baseUrl . '/oauth2/authorize';
+		$this->oauth['token'] = $baseUrl . '/oauth2/token';
+		$this->oauth['revoke'] = $baseUrl . '/oauth2/revoke';
+
+		// Set up API endpoints with version
+		$this->apiEndpoints = [
+			'locations' => '/v2/locations',
+			'customers' => '/v2/customers',
+			'catalog' => '/v2/catalog',
+			'payments' => '/v2/payments',
+			'orders' => '/v2/orders',
+			'inventory' => '/v2/inventory'
+		];
+
+	}
+
+	public function getOrderFields():array
+	{
+		return [
+					'post_title' => [
+						'type' => 'text',
+						'label' => 'Order Number'
+					],
+					'square_order_id' => [
+						'type' => 'text',
+						'label' => 'Square Order ID',
+					],
+					'square_payment_id' => [
+						'type' => 'text',
+						'label' => 'Square Payment ID',
+					],
+					'square_customer_id' => [
+						'type' => 'text',
+						'label' => 'Square Customer ID',
+					],
+					'amount' => [
+						'type' => 'number',
+						'label' => 'Total Amount (cents)',
+					],
+					'square_payment_status' => [
+						'type' => 'select',
+						'label' => 'Order Status',
+						'options' => [
+							'PROPOSED' => 'Proposed',
+							'RESERVED' => 'Reserved',
+							'PREPARED' => 'Prepared (Ready for Pickup)',
+							'COMPLETED' => 'Completed',
+							'CANCELED' => 'Canceled'
+						],
+					],
+					'fulfillment_status' => [
+						'type' => 'select',
+						'label' => 'Fulfillment Status',
+						'options' => [
+							'PROPOSED' => 'Proposed',
+							'RESERVED' => 'Reserved',
+							'PREPARED' => 'Prepared',
+							'COMPLETED' => 'Completed',
+							'CANCELED' => 'Canceled',
+							'FAILED' => 'Failed'
+						],
+					],
+					'pickup_time' => [
+						'type' => 'datetime',
+						'label' => 'Pickup Time'
+					],
+					'customer_email' => [
+						'type' => 'email',
+						'label' => 'Customer Email',
+					],
+					'customer_name' => [
+						'type' => 'text',
+						'label' => 'Customer Name',
+					],
+					'customer_phone' => [
+						'type' => 'phone',
+						'label' => 'Customer Phone',
+						'section'=> 'your-account'
+					],
+					'special_instructions' => [
+						'type' => 'textarea',
+						'label' => 'Special Instructions',
+					],
+					'items' => [
+						'type' => 'repeater',
+						'label' => 'Order Items',
+						'fields' => [
+							'name' => ['type' => 'text', 'label' => 'Item Name'],
+							'quantity' => ['type' => 'number', 'label' => 'Quantity'],
+							'price' => ['type' => 'number', 'label' => 'Price'],
+							'note' => ['type' => 'text', 'label' => 'Note']
+						]
+					],
+					'receipt_url' => [
+						'type' => 'url',
+						'label' => 'Receipt URL',
+					],
+					'created_at' => [
+						'type' => 'datetime',
+						'label' => 'Created At',
+					],
+					'updated_at' => [
+						'type' => 'datetime',
+						'label' => 'Last Updated',
+					]
+				];
+	}
+
+	public function registerSquarePostTypes():void
+	{
+		$orders = Registrar::forPost($this->orderPostType, 'Square Order', 'Square Orders');
+		$orders->make([
+			'public'	=> true
+		]);
+		$orders->setAll(['system']);
+
+		$fields = $orders->fields();
+		foreach ($this->getOrderFields() as $fieldName => $config) {
+			$fields->addField($fieldName, $config);
+		}
+	}
+
+	/**
+	 * Get request headers for API calls
+	 */
+	protected function getRequestHeaders(): array
+	{
+		return [
+			'Authorization' => 'Bearer ' . ($this->credentials['access_token'] ?? ''),
+			'Square-Version' => $this->apiVersion,
+			'Content-Type' => 'application/json'
+		];
+	}
+
+	/**
+	 * Add Square-specific OAuth parameters
+	 */
+	protected function addOAuthParams(array $params): array
+	{
+		// Load current environment setting
+		$this->ensureInitialized();
+
+		// For production, add session=false
+		// For sandbox, session parameter is not supported and should be omitted
+		if ($this->environment === 'production') {
+			$params['session'] = 'false';
+		} else {
+			// Ensure session is not included for sandbox
+			unset($params['session']);
+		}
+
+		return $params;
+	}
+
+	/**
+	 * Exchange OAuth code for tokens
+	 * Override to handle Square's specific response format
+	 */
+	protected function exchangeOAuthCode(string $code): ?array
+	{
+		$this->ensureInitialized();
+
+		// Prepare the request body as an array
+		$body = [
+			'client_id' => $this->credentials['client_id'] ?? '',
+			'client_secret' => $this->credentials['client_secret'] ?? '',
+			'code' => $code,
+			'grant_type' => 'authorization_code',
+			'redirect_uri' => $this->getRedirectUri()
+		];
+
+		$response = wp_remote_post($this->oauth['token'], [
+			'body' => json_encode($body),
+			'headers' => [
+				'Content-Type' => 'application/json',
+				'Square-Version' => $this->apiVersion
+			]
+		]);
+
+		if (is_wp_error($response)) {
+			$this->logError('OAuth token exchange failed', ['error' => $response->get_error_message()]);
+			return null;
+		}
+
+		$data = json_decode(wp_remote_retrieve_body($response), true);
+		if (isset($data['access_token'])) {
+			return [
+				'access_token' => $data['access_token'],
+				'refresh_token' => $data['refresh_token'] ?? '',
+				'expires_in' => $data['expires_in'] ?? 2592000, // 30 days default
+				'token_type' => $data['token_type'] ?? 'Bearer',
+				'merchant_id' => $data['merchant_id'] ?? '',
+				'scope' => $data['scope'] ?? ''
+			];
+		}
+
+		$this->logError('Failed to obtain access token', ['response' => $data]);
+		return null;
+	}
+
+	/**
+	 * Add Square-specific credential data after OAuth
+	 */
+	protected function addCredentialData(array $credentials, array $tokens): array
+	{
+		// Store Square-specific data
+		$credentials['merchant_id'] = $tokens['merchant_id'] ?? '';
+
+		// Preserve selected location if it exists
+		$credentials['location_id'] = $this->credentials['location_id'] ?? '';
+
+		// Preserve sandbox setting
+		$credentials['environment'] = $this->credentials['environment'] ?? 'sandbox';
+
+		return $credentials;
+	}
+
+	/**
+	 * Refresh OAuth token
+	 * Override to handle Square's refresh token flow
+	 */
+	protected function refreshOAuthToken(): bool
+	{
+		if (!$this->isOAuthService || empty($this->credentials['refresh_token'])) {
+			return false;
+		}
+
+		$response = wp_remote_post($this->oauth['token'], [
+			'body' => [
+				'client_id' => $this->credentials['client_id'],
+				'client_secret' => $this->credentials['client_secret'],
+				'refresh_token' => $this->credentials['refresh_token'],
+				'grant_type' => 'refresh_token'
+			],
+			'headers' => $this->getRequestHeaders()
+		]);
+
+		if (is_wp_error($response)) {
+			$this->logError('Failed to refresh Square token', [
+				'error' => $response->get_error_message()
+			]);
+			return false;
+		}
+
+		$data = json_decode(wp_remote_retrieve_body($response), true);
+
+		if (isset($data['access_token'])) {
+			$this->credentials['access_token'] = $data['access_token'];
+			$this->credentials['expires_at'] = time() + ($data['expires_in'] ?? 2592000); // 30 days
+			// Note: Square returns the SAME refresh token
+			if (isset($data['refresh_token'])) {
+				$this->credentials['refresh_token'] = $data['refresh_token'];
+			}
+
+			$this->saveCredentials($this->credentials);
+			return true;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Load Square locations
+	 */
+	protected function loadLocations(): void
+	{
+		// Skip if we don't have credentials yet (during OAuth flow)
+		if (empty($this->credentials['access_token'])) {
+			return;
+		}
+		try {
+			$response = $this->getRequest(
+				'/v2/locations',
+				[],
+				$this->environment
+			);
+
+			if (isset($response['locations'])) {
+				$this->locations = $response['locations'];
+
+				// Update location options in fields
+				$locationOptions = [];
+				foreach ($this->locations as $location) {
+					if ($location['status'] === 'ACTIVE') {
+						$locationOptions[$location['id']] = $location['name'] ?? $location['id'];
+					}
+				}
+
+				if (!empty($locationOptions)) {
+					$this->fields['location_id']['options'] = $locationOptions;
+				}
+			}
+		} catch (Exception $e) {
+			$this->logError('Failed to load locations', ['error' => $e->getMessage()]);
+			// Set empty locations array so we don't retry constantly
+			$this->locations = [];
+		}
+	}
+
+	protected function renderOAuthConnectedOptions(): void
+	{
+		if (!$this->isSetUp()) {
+			return;
+		}
+
+		// Check OAuth status
+		$oauth_valid = $this->isOAuthValid();
+
+		if (!$oauth_valid) {
+			?>
+			<div class="oauth-status">
+				<span class="status-invalid">⚠️ OAuth token expired - please reconnect</span>
+			</div>
+			<?php
+		}
+
+		$this->loadLocations();
+		if (!empty($this->locations)) {
+			?>
+			<div class="form-field" data-service="square">
+				<label for="square_location_id">Square Location</label>
+				<select name="location_id"
+						id="square_location_id"
+						class="jvb-ajax-update"
+						data-action="select_location">
+					<option value="">Select a location...</option>
+					<?php foreach ($this->locations as $location): ?>
+						<option value="<?php echo esc_attr($location['id']); ?>"
+							<?php selected($this->credentials['location_id'] ?? '', $location['id']); ?>>
+							<?php echo esc_html($location['name']); ?>
+						</option>
+					<?php endforeach; ?>
+				</select>
+			</div>
+			<?php
+
+			// Show current selection status
+			if (!empty($this->credentials['location'])) {
+				?>
+				<div class="connection-status connected">
+					✅ Connected to: <?php echo esc_html($this->credentials['location_id']); ?>
+				</div>
+				<?php
+			}
+		} else {
+			?>
+			<div class="notice notice-warning">
+				<p>No Square locations found. Please ensure your Square credentials are correct.</p>
+			</div>
+			<?php
+		}
+	}
+
+	/**
+	 * Handle location selection
+	 */
+	public function handleSelectLocation($data): array
+	{
+		if (empty($data['location_id'])) {
+			return [
+				'success' => false,
+				'message' => 'No location selected'
+			];
+		}
+
+		$this->credentials['location_id'] = sanitize_text_field($data['location_id']);
+		$this->locationId = $this->credentials['location_id'];
+
+		$result = $this->saveCredentials($this->credentials);
+
+		if ($result['success']) {
+			return [
+				'success' => true,
+				'message' => 'Location updated successfully'
+			];
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Handle fetching locations
+	 */
+	public function handleFetchLocations(): array
+	{
+		try {
+			$this->loadLocations();
+
+			if (empty($this->locations)) {
+				return [
+					'success' => false,
+					'message' => 'No locations found'
+				];
+			}
+
+			return [
+				'success' => true,
+				'data' => $this->locations,
+				'message' => sprintf('Found %d location(s)', count($this->locations))
+			];
+		} catch (Exception $e) {
+			return [
+				'success' => false,
+				'message' => 'Failed to fetch locations: ' . $e->getMessage()
+			];
+		}
+	}
+
+	/**
+	 * Handle webhook verification
+	 */
+	public function handleVerifyWebhook($data): array
+	{
+		if (empty($data['signature']) || empty($data['body'])) {
+			return [
+				'success' => false,
+				'message' => 'Missing signature or body'
+			];
+		}
+
+		$signatureKey = $this->credentials['webhook_signature_key'] ?? '';
+
+		if (empty($signatureKey)) {
+			return [
+				'success' => false,
+				'message' => 'Webhook signature key not configured'
+			];
+		}
+
+		// Calculate expected signature
+		$stringToSign = $data['url'] . $data['body'];
+		$expectedSignature = base64_encode(
+			hash_hmac('sha256', $stringToSign, $signatureKey, true)
+		);
+
+		$isValid = hash_equals($expectedSignature, $data['signature']);
+
+		return [
+			'success' => $isValid,
+			'message' => $isValid ? 'Valid webhook signature' : 'Invalid webhook signature'
+		];
+	}
+
+	/**
+	 * Test payment/connection
+	 */
+	public function handleTestPayment(): array
+	{
+		try {
+			// Test with a simple API call to verify connection
+			$response = $this->getRequest(
+				'/v2/locations/'.$this->locationId,
+				[],
+				$this->environment
+			);
+			if (!empty($response['location'])) {
+				return [
+					'success' => true,
+					'message' => 'Successfully connected to Square',
+					'data' => [
+						'location_name' => $response['location']['name'] ?? 'Unknown',
+						'merchant_id' => $this->merchantId,
+						'environment' => $this->environment
+					]
+				];
+			}
+
+			return [
+				'success' => false,
+				'message' => 'Connection test failed'
+			];
+		} catch (Exception $e) {
+			return [
+				'success' => false,
+				'message' => 'Connection test failed: ' . $e->getMessage()
+			];
+		}
+	}
+
+	/**
+	 * Create a Square customer
+	 */
+	public function createCustomer(array $customerData): array
+	{
+		try {
+			$response = $this->postRequest('/v2/customers', [
+				'given_name' => $customerData['first_name'] ?? '',
+				'family_name' => $customerData['last_name'] ?? '',
+				'email_address' => $customerData['email'] ?? '',
+				'phone_number' => $customerData['phone'] ?? '',
+				'reference_id' => $customerData['reference_id'] ?? '',
+				'note' => $customerData['note'] ?? ''
+			], $this->environment);
+
+			return [
+				'success' => true,
+				'data' => $response['customer'] ?? []
+			];
+		} catch (Exception $e) {
+			return [
+				'success' => false,
+				'message' => 'Failed to create customer: ' . $e->getMessage()
+			];
+		}
+	}
+
+	/**
+	 * Create a catalog item
+	 */
+	public function createCatalogItem(array $itemData): array
+	{
+		try {
+			$response = $this->postRequest('/v2/catalog/object',  [
+				'idempotency_key' => wp_generate_uuid4(),
+				'object' => [
+					'type' => 'ITEM',
+					'id' => '#' . sanitize_title($itemData['name']),
+					'item_data' => [
+						'name' => $itemData['name'],
+						'description' => $itemData['description'] ?? '',
+						'variations' => $this->buildItemVariations($itemData)
+					]
+				]
+			], $this->environment);
+
+			return [
+				'success' => true,
+				'data' => $response['catalog_object'] ?? []
+			];
+		} catch (Exception $e) {
+			return [
+				'success' => false,
+				'message' => 'Failed to create catalog item: ' . $e->getMessage()
+			];
+		}
+	}
+
+	/**
+	 * Build item variations for catalog
+	 */
+	protected function buildItemVariations(array $itemData): array
+	{
+		$variations = [];
+
+		if (isset($itemData['variations']) && is_array($itemData['variations'])) {
+			foreach ($itemData['variations'] as $variation) {
+				$variations[] = [
+					'type' => 'ITEM_VARIATION',
+					'id' => '#' . sanitize_title($variation['name']),
+					'item_variation_data' => [
+						'name' => $variation['name'],
+						'pricing_type' => 'FIXED_PRICING',
+						'price_money' => [
+							'amount' => $this->formatPrice($variation['_square_price']??0),
+							'currency' => $this->getCurrency()
+						]
+					]
+				];
+			}
+		} else {
+			// Default single variation
+			$variations[] = [
+				'type' => 'ITEM_VARIATION',
+				'id' => '#regular',
+				'item_variation_data' => [
+					'name' => 'Regular',
+					'pricing_type' => 'FIXED_PRICING',
+					'price_money' => [
+						'amount' => $this->formatPrice($itemData['_square_price'] ?? 0),
+						'currency' => $this->getCurrency()
+					]
+				]
+			];
+		}
+
+		return $variations;
+	}
+
+	/**
+	 * Handle OAuth disconnect
+	 */
+	public function handleOAuthDisconnect(): array
+	{
+		try {
+			// Revoke the token with Square
+			if (!empty($this->credentials['access_token'])) {
+				wp_remote_post($this->oauth['revoke'], [
+					'body' => [
+						'client_id' => $this->credentials['client_id'] ?? '',
+						'access_token' => $this->credentials['access_token']
+					],
+					'headers' => $this->getRequestHeaders()
+				]);
+			}
+
+			// Clear stored credentials
+			$this->credentials = [
+				'client_id' => $this->credentials['client_id'] ?? '',
+				'client_secret' => $this->credentials['client_secret'] ?? ''
+			];
+
+			$this->saveCredentials($this->credentials);
+			$this->clearCache();
+
+			return [
+				'success' => true,
+				'message' => 'Successfully disconnected from Square'
+			];
+		} catch (Exception $e) {
+			return [
+				'success' => false,
+				'message' => 'Failed to disconnect: ' . $e->getMessage()
+			];
+		}
+	}
+
+	/**
+	 * Register additional WordPress hooks
+	 */
+	protected function registerAdditionalHooks(): void
+	{
+		if (!$this->isSetUp()) {
+			error_log('Square is not setup');
+			return;
+		}
+
+        add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
+
+		add_filter('jvbAdditionalActions', [Checkout::class, 'render']);
+
+		add_filter('jvb_checkout_description', function (string $desc, string $provider) {
+			if ($provider === 'square') {
+				return 'Securely checkout with your name, email, and payments processed by Square.';
+			}
+			return $desc;
+		}, 10, 2);
+
+		// Square-specific pickup fields (extracted from old outputCheckout)
+		add_filter('jvb_checkout_fields', [$this, 'addPickupFields'], 10, 2);
+
+		// Browse URL for this client (restaurant menu)
+		add_filter('jvb_checkout_browse_url', function () {
+			return get_post_type_archive_link(BASE . 'menu_item');
+		});
+		add_filter('jvb_checkout_browse_text', function () {
+			return 'browse our menu';
+		});
+	}
+
+	/**
+	 * Pickup/ordering fields for the shared checkout form.
+	 * Specific to this Square client's food ordering use case.
+	 */
+	public function addPickupFields(string $html, string $provider): string
+	{
+		if ($provider !== 'square') {
+			return $html;
+		}
+
+		return $html
+			. '<h3>Pickup Details</h3>'
+			. Form::render('pickup_time', null, [
+				'type'     => 'datetime',
+				'label'    => 'Pickup Time',
+				'min'      => '11:00',
+				'max'      => '20:00',
+				'required' => true,
+			])
+			. Form::render('special_instructions', null, [
+				'type'  => 'textarea',
+				'label' => 'Special Instructions',
+				'quill' => true,
+			]);
+	}
+
+	protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void
+	{
+		$queue    = JVB()->queue();
+
+		$queue->registry()->register(self::$syncCustomer, new TypeConfig(
+			executor:   $executor,
+			maxRetries: 2
+		));
+
+		$queue->registry()->register(self::$import, new TypeConfig(
+			executor:   $executor,
+			maxRetries: 3
+		));
+	}
+
+	/******************************************************************
+	 * POST SYNC METHODS
+	 ******************************************************************/
+
+	/**
+	 * Handle post save for Square sync
+	 */
+	protected function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings): void
+	{
+		error_log('==== [Square]::handleTheSavePost ====');
+		$this->queueOperation(self::$syncTo, [
+			'items'   => [$postID],
+			'user'		=> user_can($post->post_author, 'manage_options') ? null : $post->post_author
+		], [
+			'priority' => 'high',
+			'delay'    => 30,
+		]);
+		Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued');
+	}
+
+	/**
+	 * Handle post deletion
+	 */
+	public function handleDeletePost(int $postID): void
+	{
+		$item_id = $this->getServiceItemID($postID);
+
+		if (empty($item_id)) {
+			return;
+		}
+		$this->queueOperation(self::$deleteFrom, [
+			'external_ids' => [$item_id],
+			'post_id'      => $postID,
+		], [
+			'priority' => 'high',
+		]);
+
+	}
+
+	/**
+	 * Process sync to Square
+	 */
+	private function processSyncToSquare(array $data): array
+	{
+		$items = $data['items'] ?? [];
+		$success = [];
+		$errors = [];
+
+		if (empty($items)) {
+			return [
+				'success' => true,
+				'result' => [
+					'synced' => [],
+					'errors' => ['No items to sync']
+				]
+			];
+		}
+
+		//Check images are already uploaded first
+		$image_mappings = $this->batchUploadImages($items);
+
+		$catalog = [];
+		$map = [];
+		foreach ($items as $postID) {
+			$post = get_post($postID);
+			if (!$post) {
+				$errors[] = "Post $postID not found";
+				continue;
+			}
+
+			$square_image_id = $image_mappings[$postID] ?? null;
+			$catalog_object = $this->buildCatalogObject($postID, $square_image_id);
+			if (is_wp_error($catalog_object)) {
+				$errors[] = $catalog_object->get_error_message();
+				continue;
+			}
+			$map[$catalog_object['id']] = $postID;
+			$catalog[] = $catalog_object;
+		}
+
+		if (!empty($catalog)) {
+			$response = $this->postRequest('catalog/batch-upsert', [
+				'idempotency_key' => wp_generate_uuid4(),
+				'batches' => [[
+					'objects' => $catalog
+				]]
+			]);
+
+			if (!is_wp_error($response)) {
+				$this->processBatchSyncResponse($response, $map, $success, $errors);
+			} else {
+				// Handle batch request failure
+				$error_message = 'Batch sync failed';
+				$this->logError($error_message, [
+					'method' => 'processSyncToSquare',
+					'post_ids' => $items,
+					'error' => $response
+				]);
+
+				// Mark all items as failed
+				foreach ($items as $postID) {
+					if (get_post($postID)) { // Only if post exists
+						$errors[] = "Failed to sync post $postID";
+						update_post_meta($postID, BASE . '_square_sync_status', 'error');
+					}
+				}
+			}
+		}
+
+		return [
+			'success' => count($success) > 0,
+			'result' => [
+				'synced' => $success,
+				'errors' => $errors
+			]
+		];
+	}
+
+	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) {
+				$object_id = $object['id'];
+				$post_id = $map[$object_id] ?? null;
+
+				if (!$post_id) {
+					// Try to find post ID from the object ID pattern
+					if (preg_match('/#\w+_(\d+)/', $object_id, $matches)) {
+						$post_id = (int)$matches[1];
+					}
+				}
+
+				if ($post_id && get_post($post_id)) {
+					// Update post meta with Square data
+					update_post_meta($post_id, BASE . '_square_catalog_id', $object['id']);
+					update_post_meta($post_id, BASE . '_square_sync_status', 'synced');
+					update_post_meta($post_id, BASE . '_square_last_sync', current_time('mysql'));
+
+					// Save variation IDs if present
+					if (!empty($object['item_data']['variations'])) {
+						foreach ($object['item_data']['variations'] as $index => $variation) {
+							update_post_meta($post_id, BASE . '_square_variation_' . $index . '_id', $variation['id']);
+						}
+					}
+
+					$success[] = $post_id;
+				}
+			}
+		}
+
+		// Handle errors
+		if (!empty($response['errors'])) {
+			foreach ($response['errors'] as $error) {
+				$error_detail = $error['detail'] ?? 'Unknown error';
+				$error_field = $error['field'] ?? '';
+
+				// Try to extract post ID from error field if possible
+				$post_id = null;
+				if (preg_match('/batches\[0\]\.objects\[(\d+)\]/', $error_field, $matches)) {
+					$object_index = (int)$matches[1];
+					// Get post ID from the object at this index
+					if (isset($catalog_objects[$object_index])) {
+						$object_id = $catalog_objects[$object_index]['id'];
+						$post_id = $map[$object_id] ?? null;
+					}
+				}
+
+				$error_message = $post_id
+					? "Post $post_id: $error_detail"
+					: "Batch error: $error_detail";
+
+				$errors[] = $error_message;
+
+				if ($post_id) {
+					update_post_meta($post_id, BASE . '_square_sync_status', 'error');
+				}
+			}
+		}
+
+		// Handle any posts that weren't in the response (shouldn't happen but good to check)
+		foreach ($map as $object_id => $post_id) {
+			if (!in_array($post_id, $success)) {
+				$found_in_errors = false;
+				foreach ($errors as $error) {
+					if (str_contains($error, "Post $post_id")) {
+						$found_in_errors = true;
+						break;
+					}
+				}
+
+				if (!$found_in_errors) {
+					$errors[] = "Post $post_id: No response from Square";
+					update_post_meta($post_id, BASE . '_square_sync_status', 'error');
+				}
+			}
+		}
+	}
+
+	/**
+	 * Build catalog object from WordPress post
+	 *
+	 * @param int $postID WordPress post ID
+	 * @param string|null $square_image_id Previously uploaded Square image ID
+	 * @return array|WP_Error Catalog object or error
+	 */
+	//TODO: Get to work with Registrar settings
+	protected function buildCatalogObject(int $postID, ?string $square_image_id = null): array|WP_Error
+	{
+		$post = get_post($postID);
+		if (!$post) {
+			return new WP_Error('post_not_found', "Post $postID not found");
+		}
+
+		$meta = Meta::forPost($postID);
+		$post_type = get_post_type($postID);
+
+		// 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 = [
+			'type' => 'ITEM',
+			'id' => $existing_square_id ?: '#menu_item_item_' . $postID,
+			'item_data' => [
+				'name' => $post->post_title,
+				'description' => wp_strip_all_tags($post->post_content),
+				'product_type' => $product_type,
+				'variations' => [],
+				'is_taxable' => true,
+			]
+		];
+
+		// Add image ID if provided (must be already uploaded to Square)
+		if (!empty($square_image_id)) {
+			$catalog_object['item_data']['image_ids'] = [$square_image_id];
+		}
+
+		// Add variations
+		$variations = $meta->get('_square_product_variations');
+		if (empty($variations)) {
+			// Create default variation if none exist
+			$catalog_object['item_data']['variations'][] = [
+				'type' => 'ITEM_VARIATION',
+				'id' => $existing_square_id ? null : '#'.BASE.'menu_item_' . $postID . '_var_default',
+				'item_variation_data' => [
+					'name' => 'Regular',
+					'ordinal' => 0,
+					'pricing_type' => 'FIXED_PRICING',
+					'price_money' => [
+						'amount' => $this->formatPrice($meta->get('_square_price')),
+						'currency' => $this->getCurrency()
+					],
+					'sellable' => true,
+					'stockable' => true
+				]
+			];
+		} else {
+			$resetVariations = false;
+			foreach ($variations as $index => $variation) {
+				$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' => $variation['item_id'],
+					'item_variation_data' => [
+						'name' => $variation['name'] ?? 'Variation ' . ($index + 1),
+						'ordinal' => $index,
+						'pricing_type' => 'FIXED_PRICING',
+						'price_money' => [
+							'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
+		$categories = wp_get_post_terms($postID, $post_type . '_category', ['fields' => 'ids']);
+		if (!empty($categories)) {
+			$category_ids = [];
+			foreach ($categories as $term_id) {
+				$square_cat_id = get_term_meta($term_id, BASE . '_square_category_id', true);
+				if ($square_cat_id) {
+					$category_ids[] = $square_cat_id;
+				}
+			}
+			if (!empty($category_ids)) {
+				$catalog_object['item_data']['category_ids'] = $category_ids;
+			}
+		}
+
+		// Add modifiers if they exist
+		$modifiers = $meta->get('modifiers');
+		if (!empty($modifiers)) {
+			$modifier_ids = [];
+			foreach ($modifiers as $modifier) {
+				if (!empty($modifier['square_id'])) {
+					$modifier_ids[] = $modifier['square_id'];
+				}
+			}
+			if (!empty($modifier_ids)) {
+				$catalog_object['item_data']['modifier_list_info'] = array_map(function($id) {
+					return ['modifier_list_id' => $id];
+				}, $modifier_ids);
+			}
+		}
+
+		// Add tax settings
+		$tax_ids = $meta->get('tax_ids');
+		if (!empty($tax_ids)) {
+			$catalog_object['item_data']['tax_ids'] = $tax_ids;
+		}
+
+		return $catalog_object;
+	}
+
+
+
+	/**
+	 * Get variation mapping for post type
+	 */
+	protected function getVariationMapping(string $post_type): array
+	{
+		$registrar = Registrar::getInstance($post_type);
+		if (!$registrar) {
+			return [];
+		}
+		$config = $registrar->getIntegrationConfig($this->service_name);
+		$product_type = $config['content_type']??'REGULAR';
+		$valid_fields = $this->getValidFieldsForProductType($product_type);
+
+		$defaults = [
+			'name' => 'name',
+			'id' => '_square_item_id',
+			'sku' => '_square_sku',
+			'price' => '_square_price',
+			'track_inventory' => '_square_track_inventory',
+			'service_duration' => '_square_service_duration',
+			'available_for_booking' => '_square_available_for_booking',
+			'gift_card_type' => '_square_gift_card_type',
+			'ingredients' => '_square_ingredients',
+			'preparation_time_duration' => '_square_preparation_time_duration'
+		];
+
+		$extended = apply_filters(
+			BASE . $this->service_name . '_variation_mapping',
+			$defaults,
+			$post_type,
+			$this
+		);
+
+		return array_filter($extended, function($key) use ($valid_fields) {
+			return in_array($key, $valid_fields);
+		}, ARRAY_FILTER_USE_KEY);
+	}
+
+
+
+	/**
+	 * Get or create Square category
+	 */
+	private function getOrCreateSquareCategory(string $name): ?string
+	{
+		// Check cached mapping
+		$cached_id = get_option(BASE . 'square_category_' . sanitize_title($name));
+		if ($cached_id) {
+			return $cached_id;
+		}
+
+		// Create new category
+		$response = $this->postRequest('catalog/batch-upsert', [
+			'idempotency_key' => wp_generate_uuid4(),
+			'batches' => [[
+				'objects' => [[
+					'type' => 'CATEGORY',
+					'id' => '#category_' . sanitize_title($name),
+					'category_data' => [
+						'name' => $name
+					]
+				]]
+			]]
+		]);
+
+		if (!is_wp_error($response) && !empty($response['objects'][0]['id'])) {
+			$category_id = $response['objects'][0]['id'];
+			update_option(BASE . 'square_category_' . sanitize_title($name), $category_id);
+			return $category_id;
+		}
+
+		return null;
+	}
+
+	/**
+	 * Get field mapping for post type
+	 */
+	protected function getFieldMapping(string $post_type): array
+	{
+		$registrar = Registrar::getInstance($post_type);
+		if (!$registrar) {
+			return [];
+		}
+		$config = $registrar->getIntegrationConfig($this->service_name);
+		$product_type = $config['content_type']??'REGULAR';
+		$valid_fields = $this->getValidFieldsForProductType($product_type);
+
+		$defaults = [
+			'name' => 'post_title',
+			'description_html' => 'post_content',
+			'abbreviation' => 'abbreviation',
+			'id' => '_square_catalog_id',
+			'sku' => '_square_sku',
+			'category_id' => 'category',
+			'image_ids' => 'post_thumbnail',
+			'price' => '_square_price',
+			'tax_ids' => 'tax_ids',
+			// Availability
+			'available_online' => '_square_available_online',
+			'available_for_pickup' => '_square_available_for_pickup',
+			'available_electronically' => '_square_available_electronically',
+			// Modifiers
+			'modifier_list_info' => 'modifiers',
+			// Item options
+			'item_options' => 'options',
+			// Reporting
+			'reporting_category' => 'reporting_category',
+		];
+
+		$extended = apply_filters(
+			BASE . $this->service_name . '_field_mapping',
+			$defaults,
+			$post_type,
+			$this
+		);
+
+		return array_filter($extended, function($key) use ($valid_fields) {
+			return in_array($key, $valid_fields);
+		}, ARRAY_FILTER_USE_KEY);
+	}
+
+	/**
+	 * Get valid fields for Square product type
+	 */
+	//TODO: This feels redundant now, with how we've defined fields in getAdditionalFields
+	private function getValidFieldsForProductType(string $product_type): array
+	{
+		$fields = ['name', 'description_html', 'sku', 'price', 'image_ids', 'category_id'];
+
+		$type_specific = [
+			'FOOD_AND_BEV' => [
+				'ingredients',
+				'preparation_time_duration',
+				'dietary_preferences',
+				'calories_text'
+			],
+			'APPOINTMENTS_SERVICE' => [
+				'service_duration',
+				'available_for_booking',
+				'team_member_ids',
+				'booking_availability'
+			],
+			'EVENT' => [
+				'start_date',
+				'end_date',
+				'venue_details',
+				'capacity'
+			],
+			'GIFT_CARD' => [
+				'gift_card_type',
+				'allowed_locations'
+			]
+		];
+
+		if (isset($type_specific[$product_type])) {
+			$fields = array_merge($fields, $type_specific[$product_type]);
+		}
+
+		return $fields;
+	}
+
+	/******************************************************************
+	 * CUSTOMER MANAGEMENT
+	 ******************************************************************/
+
+	/**
+	 * Handle customer authentication during checkout
+	 */
+	//TODO: Is this necessary?
+	public function handleCustomerAuth($data):WP_Error|array
+	{
+		$email = sanitize_email($data['email'] ?? '');
+		$action = sanitize_text_field($data['action_type'] ?? 'check');
+
+		if (!$email) {
+			return new WP_Error('error', 'Email required');
+		}
+
+		switch ($action) {
+			case 'check':
+				return $this->checkCustomerExists($email);
+			case 'create':
+				return $this->createCustomerAccount($email);
+			default:
+				$this->logError('No action set for customer auth: '.$action, [
+					'method' => 'handleCustomerAuth'
+				]);
+				return new WP_Error('error', 'No action set for customer auth');
+		}
+
+	}
+
+
+
+	/**
+	 * Check if customer exists
+	 */
+	private function checkCustomerExists(string $email):WP_Error|array
+	{
+		// Check WordPress user
+		$user = get_user_by('email', $email);
+
+		if ($user) {
+			// Check if user has Square customer integration
+			$square_customer_id = get_user_meta($user->ID, BASE . '_square_customer_id', true);
+
+			if ($square_customer_id) {
+				return [
+					'exists'	=> true,
+					'has_account'	=> true,
+					'message' => 'Account found. Please enter your password to continue'
+				];
+			} else {
+				return [
+					'exists' => true,
+					'has_account' => false,
+					'message' => 'Email found. Would you like to create an account to save your order history?'
+				];
+			}
+		}
+
+			// Check Square for customer
+			$response = $this->postRequest('customers/search', [
+				'filter' => [
+					'email_address' => [
+						'exact' => $email
+					]
+				]
+			]);
+
+			if (!is_wp_error($response) && !empty($response['customers'])) {
+				return [
+					'exists' => true,
+					'has_account' => false,
+					'square_only' => true,
+					'message' => 'Found your previous orders. Create an account to access them?'
+				];
+			} else {
+				return [
+					'exists' => false,
+					'message' => 'New customer'
+				];
+			}
+	}
+
+	private function createCustomerAccount(string $email):WP_Error|array
+	{
+		// Validate email format
+		if (!is_email($email)) {
+			return new WP_Error('error', 'Invalid email address');
+		}
+
+		// Check if user already exists
+		if (email_exists($email)) {
+			return [
+				'success' => false,
+				'exists' => true,
+				'message' => 'An account with this email already exists. Please log in instead.'
+			];
+		}
+
+		// Create user account without password (they'll set it via email)
+		$user_id = wp_create_user(
+			$email,
+			wp_generate_password(20, true, true), // Temporary random password
+			$email
+		);
+
+		if (is_wp_error($user_id)) {
+			$this->logError('Failed to create customer account', [
+				'email' => $email,
+				'error' => $user_id->get_error_message()
+			]);
+			return new WP_Error('error', 'Failed to create account. Please try again.');
+		}
+
+		// Set user role (assuming you have a customer role defined)
+		$user = new WP_User($user_id);
+		$user->set_role(BASE.'foodie');
+
+		// Generate password reset key
+		$reset_key = get_password_reset_key($user);
+
+		if (is_wp_error($reset_key)) {
+			return new WP_Error('error', 'Account created, but couldn\'t send email. Please use password reset.');
+		}
+
+
+		// Send welcome email with password setup link
+		$this->sendWelcomeEmail($user, $reset_key);
+
+		// Link to Square customer if exists
+		$square_customer_id = $this->getOrCreateSquareCustomer([
+			'email' => $email,
+			'name' => $email
+		]);
+
+		if ($square_customer_id) {
+			update_user_meta($user_id, BASE . '_square_customer_id', $square_customer_id);
+		}
+
+		return [
+			'success' => true,
+			'message' => 'Account created! Check your email to set your password.',
+			'user_id' => $user_id
+		];
+	}
+
+	/**
+	 * Send welcome email with password setup
+	 */
+	private function sendWelcomeEmail(WP_User $user, string $reset_key): void
+	{
+		$site_name = get_bloginfo('name');
+		$reset_url = get_home_url(null, "wp-login.php?action=rp&key=$reset_key&login=" . rawurlencode($user->user_login), 'login');
+
+		$message = sprintf(
+			"Welcome to %s!\n\n" .
+			"Your account has been created. Please click the button below to set your password:\n\n" .
+			"%s\n\n" .
+			"Or, copy and paste the link below:\n\n".
+			"%s\n\n" .
+			"Once you've set your password, you can:\n" .
+			"- View your order history\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",
+			$site_name,
+			JVB()->email()->button('Reset Password', $reset_url),
+			JVB()->email()->link($reset_url),
+		);
+
+		JVB()->email()->sendEmail(
+			$user->user_email,
+			sprintf('[%s] Welcome! Set Your Password', $site_name),
+			$message
+		);
+	}
+
+	/******************************************************************
+	 * WEBHOOK HANDLING
+	 ******************************************************************/
+
+	/**
+	 * Validate webhook signature
+	 */
+	protected function validateWebhook(array $payload): bool
+	{
+		// Get signature from headers
+		$signature = $_SERVER['HTTP_X_SQUARE_SIGNATURE'] ?? '';
+
+		// If no signature provided by Square, validation fails
+		if (empty($signature)) {
+			$this->logDebug('No webhook signature provided by Square');
+			return false;
+		}
+
+		// If webhook signature key is not configured, we can't validate
+		// but we might want to process the webhook anyway (less secure)
+		if (empty($this->webhook_signature_key)) {
+			$this->logDebug('Webhook signature key not configured - processing without validation');
+			// You might want to return false here for stricter security
+			// return false;
+			return true; // Process webhook but log that it's unverified
+		}
+
+		// Get the raw request body
+		$body = file_get_contents('php://input');
+
+		// Calculate expected signature
+		$expected = base64_encode(hash_hmac('sha256', $body, $this->webhook_signature_key, true));
+
+		// Use timing-safe comparison
+		$is_valid = hash_equals($expected, $signature);
+
+		if (!$is_valid) {
+			$this->logError('Invalid webhook signature', [
+				'provided' => substr($signature, 0, 10) . '...',
+				'expected' => substr($expected, 0, 10) . '...'
+			]);
+		}
+
+		return $is_valid;
+	}
+
+	/**
+	 * Process webhook event
+	 */
+	protected function processWebhook(array $payload): bool
+	{
+		$event_type = $payload['type'] ?? '';
+		$data = $payload['data'] ?? [];
+
+		switch ($event_type) {
+			case 'payment.created':
+			case 'payment.updated':
+				return $this->handlePaymentWebhook($data);
+
+			case 'order.created':
+			case 'order.updated':
+			case 'order.fulfillment.updated':
+				return $this->handleOrderWebhook($data);
+
+			case 'catalog.version.updated':
+				return $this->handleCatalogWebhook($data);
+
+			case 'customer.created':
+			case 'customer.updated':
+				return $this->handleCustomerWebhook($data);
+
+			default:
+				$this->logDebug('Unhandled webhook type', ['type' => $event_type]);
+				return true;
+		}
+	}
+
+	/**
+	 * Handle order status webhook
+	 */
+	private function handleOrderWebhook(array $data): bool
+	{
+		$order_id = $data['object']['order']['id'] ?? '';
+		$state = $data['object']['order']['state'] ?? '';
+		$fulfillments = $data['object']['order']['fulfillments'] ?? [];
+
+		if (!$order_id) {
+			return false;
+		}
+
+		// Find the WP post for this order
+		$wp_order_id = $this->getOrderPost($order_id);
+
+		if ($wp_order_id) {
+			// Update the post meta
+			$meta = Meta::forPost($wp_order_id);
+			$updates = [
+				'status' => $state,
+				'updated_at' => current_time('mysql')
+			];
+
+			// Extract fulfillment status and pickup time
+			if (!empty($fulfillments[0])) {
+				$fulfillment = $fulfillments[0];
+				$updates['fulfillment_status'] = $fulfillment['state'] ?? $state;
+
+				if (!empty($fulfillment['pickup_details']['pickup_at'])) {
+					$updates['pickup_time'] = $fulfillment['pickup_details']['pickup_at'];
+				}
+			}
+
+			$meta->setAll($updates);
+
+			// Trigger notification to customer if order is ready
+			if ($state === 'PREPARED') {
+				do_action(BASE . 'square_order_ready', $wp_order_id, $order_id);
+			}
+		}
+		// Trigger action for other integrations
+		do_action(BASE . 'square_order_updated', $order_id, $state, $data);
+
+		return true;
+	}
+
+	/******************************************************************
+	 * CONNECTION TESTING
+	 ******************************************************************/
+	protected function performConnectionTest(): bool
+	{
+		try {
+			$response = $this->getRequest('/v2/merchants/me');
+			if (is_wp_error($response)) {
+				$this->logError('Connection test failed', ['error' => $response->get_error_message()]);
+				return false;
+			}
+
+			// Check if we got valid merchant data
+			return !empty($response['merchant']);
+		} catch (Exception $e) {
+			$this->logError('Connection test failed', ['error' => $e->getMessage()]);
+			return false;
+		}
+	}
+
+	/**
+	 * Check OAuth connection status
+	 */
+	protected function checkOAuthStatus(): array
+	{
+		if (!$this->hasOAuthCredentials()) {
+			return [
+				'success' => false,
+				'message' => 'OAuth not configured. Please authorize with Square.',
+				'authorized' => false
+			];
+		}
+
+		// Test the connection
+		if ($this->performConnectionTest()) {
+			$merchant = $this->getRequest('merchants/me');
+			return [
+				'success' => true,
+				'message' => 'Successfully connected to Square',
+				'authorized' => true,
+				'merchant' => $merchant['merchant'] ?? []
+			];
+		}
+
+		return [
+			'success' => false,
+			'message' => 'OAuth token may be expired. Please re-authorize.',
+			'authorized' => false
+		];
+	}
+
+	/******************************************************************
+	 * ADMIN UI
+	 ******************************************************************/
+	/**
+	 * Enqueue checkout scripts with Square configuration
+	 */
+	public function enqueueScripts(): void
+	{
+		jvbInlineStyles('forms');
+		$this->loadCredentials();
+		$sdk_url = $this->environment === 'production'
+			? 'https://web.squarecdn.com/v1/square.js'
+			: 'https://sandbox.web.squarecdn.com/v1/square.js';
+
+		wp_enqueue_script(
+			'square-payments-sdk',
+			$sdk_url,
+			[],
+			null,
+			['strategy' => 'defer', 'in_footer' => true]
+		);
+
+		// Shared cart checkout base class
+		wp_register_script(
+			'jvb-checkout',
+			JVB_URL . 'assets/js/min/checkout.min.js',
+			['jvb-utility', 'jvb-queue', 'jvb-a11y', 'jvb-cache', 'jvb-tabs', 'jvb-popup', 'jvb-login'],
+			'1.1.32',
+			['strategy' => 'defer', 'in_footer' => true]
+		);
+
+		// Square checkout extends CartCheckout
+		wp_register_script(
+			'jvb-square-checkout',
+			JVB_URL . 'assets/js/min/square.min.js',
+			['jvb-checkout', 'square-payments-sdk'],
+			'1.1.32',
+			['strategy' => 'defer', 'in_footer' => true]
+		);
+
+		wp_enqueue_script('jvb-square-checkout');
+
+		wp_localize_script('jvb-square-checkout', 'squareConfig', [
+			'isOpen'         => jvbIsOpen()?'1':'0',
+			'application_id' => $this->credentials['client_id'] ?? '',
+			'location_id'    => $this->locationId,
+			'environment'    => $this->environment,
+			'currency'       => $this->getCurrency(),
+			'is_logged_in'   => is_user_logged_in(),
+			'user_email'     => is_user_logged_in() ? wp_get_current_user()->user_email : '',
+		]);
+	}
+
+	/******************************************************************
+	 * HELPER METHODS
+	 ******************************************************************/
+
+	/**
+	 * Get or create Square customer
+	 */
+	public function getOrCreateSquareCustomer(array $customer_info): ?string
+	{
+		if (empty($customer_info['email'])) {
+			return null;
+		}
+
+		// Search for existing customer
+		$search_response = $this->postRequest('customers/search', [
+			'filter' => [
+				'email_address' => [
+					'exact' => $customer_info['email']
+				]
+			]
+		]);
+
+		if (!is_wp_error($search_response) && !empty($search_response['customers'])) {
+			return $search_response['customers'][0]['id'];
+		}
+
+		// Create new customer
+		$create_response = $this->postRequest('customers', [
+			'given_name' => $customer_info['name'] ?? '',
+			'email_address' => $customer_info['email'],
+			'phone_number' => $customer_info['phone'] ?? ''
+		]);
+
+		if (!is_wp_error($create_response) && !empty($create_response['customer']['id'])) {
+			return $create_response['customer']['id'];
+		}
+
+		return null;
+	}
+
+
+	/**
+	 * Process delete from Square
+	 */
+	private function processDeleteFromSquare(array $data): array
+	{
+		$square_ids = $data['square_ids'] ?? [];
+
+		if (empty($square_ids)) {
+			return [
+				'success' => false,
+				'result' => ['error' => 'No Square IDs provided']
+			];
+		}
+
+		$response = $this->postRequest('catalog/batch-delete', [
+			'object_ids' => $square_ids
+		]);
+
+		if (is_wp_error($response)) {
+			return [
+				'success' => false,
+				'result' => ['error' => $response->get_error_message()]
+			];
+		}
+
+		// Clean up post meta
+		if (!empty($data['post_id'])) {
+			delete_post_meta($data['post_id'], BASE . '_square_catalog_id');
+			delete_post_meta($data['post_id'], BASE . '_square_sync_status');
+		}
+
+		return [
+			'success' => true,
+			'result' => ['deleted' => $square_ids]
+		];
+	}
+
+	/**
+	 * Process sync from Square
+	 */
+	private function processSyncFromSquare(array $data): array
+	{
+		$cursor = $data['cursor'] ?? null;
+		$types = $data['types'] ?? ['ITEM'];
+
+		$request_data = [
+			'types' => implode(',', $types),
+			'limit' => 100
+		];
+
+		if ($cursor) {
+			$request_data['cursor'] = $cursor;
+		}
+
+		$response = $this->getRequest('catalog/list?' . http_build_query($request_data));
+
+		if (is_wp_error($response)) {
+			return [
+				'success' => false,
+				'result' => ['error' => $response->get_error_message()]
+			];
+		}
+
+		$imported = 0;
+		$errors = [];
+
+		foreach ($response['objects'] ?? [] as $object) {
+			if ($object['type'] === 'ITEM') {
+				$result = $this->importSquareItem($object);
+				if ($result) {
+					$imported++;
+				} else {
+					$errors[] = $object['id'];
+				}
+			}
+		}
+
+		// Queue next batch if cursor exists
+		if (!empty($response['cursor'])) {
+			$this->queueOperation('sync_from_square', [
+				'cursor' => $response['cursor'],
+				'types' => $types
+			]);
+		}
+
+		return [
+			'success' => true,
+			'result' => [
+				'imported' => $imported,
+				'errors' => $errors,
+				'has_more' => !empty($response['cursor'])
+			]
+		];
+	}
+
+	/**
+	 * Import Square item to WordPress
+	 */
+	private function importSquareItem(array $item): bool|int
+	{
+		//TODO: We need to add the post type to custom meta for Square, this is not good if we have multiple post types with the same product type
+		// Find matching content type
+		$product_type = $item['item_data']['product_type'] ?? 'REGULAR';
+		$post_type = null;
+
+		foreach (Registrar::getRegistered() as $registrar) {
+			if (!$registrar->hasIntegration($this->service_name)) {
+				continue;
+			}
+			$config = $registrar->getIntegration($this->service_name);
+			if ($config->getContent_type() && $config->getContent_type() === $product_type) {
+				$post_type = jvbCheckBase($registrar->getSlug());
+				break;
+			}
+
+		}
+
+		if (!$post_type) {
+			return false;
+		}
+
+		// Check if item already exists
+		$existing = get_posts([
+			'post_type' => $post_type,
+			'meta_key' => BASE . '_square_catalog_id',
+			'meta_value' => $item['id'],
+			'posts_per_page' => 1
+		]);
+
+		$post_data = [
+			'post_title' => $item['item_data']['name'] ?? '',
+			'post_content' => $item['item_data']['description'] ?? '',
+			'post_type' => $post_type,
+			'post_status' => 'publish'
+		];
+
+		if (!empty($existing)) {
+			$post_data['ID'] = $existing[0]->ID;
+			$post_id = wp_update_post($post_data);
+		} else {
+			$post_id = wp_insert_post($post_data);
+		}
+
+		if (!$post_id || is_wp_error($post_id)) {
+			return false;
+		}
+
+		// Map and save meta fields
+		$this->mapSquareFieldsToWordPress($post_id, $item);
+
+		return $post_id;
+	}
+
+	/**
+	 * Map Square fields to WordPress meta
+	 */
+	private function mapSquareFieldsToWordPress(int $post_id, array $item): void
+	{
+		$meta = Meta::forPost($post_id);
+		$field_map = $this->getFieldMapping(get_post_type($post_id));
+
+		$values_to_save = [];
+
+		// Save Square ID
+		$values_to_save['_square_catalog_id'] = $item['id'];
+		$values_to_save['_square_last_sync'] = current_time('mysql');
+
+		// Handle variations if present
+		if (!empty($item['item_data']['variations'])) {
+			$variations_data = [];
+
+			foreach ($item['item_data']['variations'] as $index => $variation) {
+				$var_data = [
+					'name' => $variation['item_variation_data']['name'] ?? '',
+					'sku' => $variation['item_variation_data']['_square_sku'] ?? '',
+				];
+
+				// Extract price
+				if (!empty($variation['item_variation_data']['price_money'])) {
+					$var_data['price'] = $variation['item_variation_data']['price_money']['amount'] / 100;
+				}
+
+				// Extract other variation fields
+				$variation_map = $this->getVariationMapping(get_post_type($post_id));
+				foreach ($variation_map as $square_field => $wp_field) {
+					if (isset($variation['item_variation_data'][$square_field])) {
+						$var_data[$wp_field] = $variation['item_variation_data'][$square_field];
+					}
+				}
+
+				$variations_data[] = $var_data;
+
+				// Save variation Square ID
+				update_post_meta($post_id, BASE . '_square_variation_' . $index . '_id', $variation['id']);
+			}
+
+			$values_to_save['product_variations'] = $variations_data;
+		}
+
+		// Map other fields
+		foreach ($field_map as $square_field => $wp_field) {
+			if ($square_field !== 'price' && isset($item['item_data'][$square_field])) {
+				$values_to_save[$wp_field] = $item['item_data'][$square_field];
+			}
+		}
+
+		// Save all values at once
+		$meta->setAll($values_to_save);
+	}
+
+	/**
+	 * Process customer sync
+	 */
+	private function processSyncCustomer(array $data): array
+	{
+		$user_id = $data['user_id'] ?? 0;
+
+		if (!$user_id) {
+			return [
+				'success' => false,
+				'result' => ['error' => 'No user ID provided']
+			];
+		}
+
+		$user = get_user_by('ID', $user_id);
+		if (!$user) {
+			return [
+				'success' => false,
+				'result' => ['error' => 'User not found']
+			];
+		}
+
+		// Get or create Square customer
+		$square_customer_id = $this->getOrCreateSquareCustomer([
+			'email' => $user->user_email,
+			'name' => $user->display_name
+		]);
+
+		if ($square_customer_id) {
+			update_user_meta($user_id, BASE . '_square_customer_id', $square_customer_id);
+
+			return [
+				'success' => true,
+				'result' => ['customer_id' => $square_customer_id]
+			];
+		}
+
+		return [
+			'success' => false,
+			'result' => ['error' => 'Could not sync customer']
+		];
+	}
+
+	/**
+	 * Handle catalog webhook
+	 */
+	private function handleCatalogWebhook(array $data): bool
+	{
+		// Queue sync from Square for updated items
+		$this->queueOperation('import_catalog', [
+			'types' => ['ITEM'],
+			'updated_at' => $data['object']['catalog_version']['updated_at'] ?? null
+		], [
+			'delay' => 60 // Wait a minute to batch multiple updates
+		]);
+
+		return true;
+	}
+
+	/**
+	 * Handle customer webhook
+	 */
+	private function handleCustomerWebhook(array $data): bool
+	{
+		$square_customer_id = $data['object']['customer']['id'] ?? '';
+		$email = $data['object']['customer']['email_address'] ?? '';
+
+		if (!$square_customer_id || !$email) {
+			return false;
+		}
+
+		// Find WordPress user with this Square customer ID
+		$users = get_users([
+			'meta_key' => BASE . '_square_customer_id',
+			'meta_value' => $square_customer_id,
+			'number' => 1
+		]);
+
+		if (!empty($users)) {
+			// Update user meta with latest Square data
+			$user = $users[0];
+			update_user_meta($user->ID, BASE . '_square_customer_updated', current_time('mysql'));
+
+			// Clear cached customer data
+			$this->cache->forget('square_customer_' . $user->ID);
+		}
+
+		return true;
+	}
+
+	/**
+	 * Handle payment webhook
+	 */
+	private function handlePaymentWebhook(array $data): bool
+	{
+		$payment_id = $data['object']['payment']['id'] ?? '';
+		$order_id = $data['object']['payment']['order_id'] ?? '';
+		$status = $data['object']['payment']['status'] ?? '';
+
+		if (!$payment_id) {
+			return false;
+		}
+
+		if ($order_id) {
+			$order = $this->getOrderPost($order_id);
+			if ($order) {
+				Meta::forPost($order)->set('square_payment_status', $status);
+			}
+		}
+
+		// Trigger action for other integrations
+		do_action(BASE . 'square_payment_updated', $payment_id, $status, $order_id, $data);
+
+		return true;
+	}
+
+	/**
+	 * Get the appropriate API base URL
+	 */
+	protected function getApiUrl(string $endpoint, ?string $baseKey = null): string
+	{
+		return rtrim($this->apiBase[$this->environment], '/') . '/' . ltrim($endpoint, '/');
+	}
+
+	/**
+	 * Handle importing catalog from Square
+	 */
+	protected function handleImportFromSquare(): array
+	{
+		if (!$this->locationId) {
+			return [
+				'success' => false,
+				'message' => 'Please select a location first'
+			];
+		}
+
+		$this->queueOperation('import_catalog', [
+			'types' => ['ITEM'],
+			'user_id' => $this->userID
+		], [
+			'priority' => 'normal'
+		]);
+
+		return [
+			'success' => true,
+			'message' => 'Import from Square queued'
+		];
+	}
+	/**
+	 * Handle syncing to Square
+	 */
+	protected function handleSyncToSquare(): array
+	{
+		if (!$this->locationId) {
+			return [
+				'success' => false,
+				'message' => 'Please select a location first'
+			];
+		}
+
+		$post_types = array_map(function($type) {
+			return jvbCheckBase($type);
+		}, $this->syncPostTypes);
+
+		// Get all posts to sync
+		$posts = get_posts([
+			'post_type' => $post_types,
+			'posts_per_page' => -1,
+			'post_status' => 'publish'
+		]);
+
+		$post_ids = wp_list_pluck($posts, 'ID');
+
+		// Queue sync operation
+		$this->queueOperation('sync_to_square', [
+			'items' => $post_ids,
+			'user_id' => $this->userID
+		], [
+			'priority' => 'normal'
+		]);
+
+		return [
+			'success' => true,
+			'message' => sprintf('Queued %d items for sync to Square', count($post_ids))
+		];
+	}
+	/**
+	 * Refresh category mappings from Square
+	 */
+	protected function handleRefreshCategories(): array
+	{
+		// Refresh category mappings from Square
+		$response = $this->getRequest('catalog/list?types=CATEGORY');
+
+		if (!is_wp_error($response) && isset($response['objects'])) {
+			$count = 0;
+			foreach ($response['objects'] as $category) {
+				if ($category['type'] === 'CATEGORY') {
+					$name = $category['category_data']['name'] ?? '';
+					if ($name) {
+						update_option(
+							BASE . 'square_category_' . sanitize_title($name),
+							$category['id']
+						);
+						$count++;
+					}
+				}
+			}
+			return [
+				'success' => true,
+				'message' => sprintf('Refreshed %d categories', $count),
+				'count' => $count
+			];
+		} else {
+			return [
+				'success' => false,
+				'message' => 'Failed to refresh categories'
+			];
+		}
+	}
+	/**
+	 * Get available Square locations
+	 */
+	protected function handleGetLocations(): array
+	{
+		// Make sure we have an access token
+		if (empty($this->access_token)) {
+			return [
+				'success' => false,
+				'message' => 'Access token required to fetch locations'
+			];
+		}
+
+		// Fetch available Square locations
+		$response = $this->getRequest('locations');
+
+		if (!is_wp_error($response) && isset($response['locations'])) {
+			$locations = array_map(function($loc) {
+				return [
+					'id' => $loc['id'],
+					'name' => $loc['name'] ?? 'Unnamed Location',
+					'address' => $loc['address']['address_line_1'] ?? '',
+					'status' => $loc['status'] ?? 'ACTIVE'
+				];
+			}, $response['locations']);
+
+			// Filter to only active locations
+			$active_locations = array_filter($locations, function($loc) {
+				return $loc['status'] === 'ACTIVE';
+			});
+
+			// Format for select field
+			if (isset($_REQUEST['for_select'])) {
+				$options = [];
+				foreach ($active_locations as $location) {
+					$label = $location['name'];
+					if ($location['address']) {
+						$label .= ' - ' . $location['address'];
+					}
+					$options[$location['id']] = $label;
+				}
+
+				return [
+					'success' => true,
+					'options' => $options
+				];
+			}
+
+			return [
+				'success' => true,
+				'locations' => array_values($active_locations)
+			];
+		}
+
+		return [
+			'success' => false,
+			'message' => 'Failed to fetch locations. Please check your access token and environment settings.'
+		];
+	}
+
+	protected function validateCredentials(array $credentials): bool
+	{
+		// For OAuth services, we need different validation based on setup stage
+		if ($this->isOAuthService) {
+			// Stage 1: Initial setup (need app credentials)
+			$hasAppCredentials = !empty($credentials['client_id'])
+				&& !empty($credentials['client_secret']);
+
+			if (!$hasAppCredentials) {
+				$this->logError('Missing required Square application credentials', [
+					'has_app_id' => !empty($credentials['client_id']),
+					'has_app_secret' => !empty($credentials['client_secret'])
+				]);
+				return false;
+			}
+
+			// Stage 2: After OAuth (should have access token)
+			// Access token might not exist yet if we're just saving app credentials
+			// before OAuth flow, so we only validate format if it exists
+			if (!empty($credentials['access_token'])) {
+				// Validate token format (Square tokens are long alphanumeric strings)
+				if (!is_string($credentials['access_token']) ||
+					strlen($credentials['access_token']) < 20) {
+					$this->logError('Invalid access token format');
+					return false;
+				}
+
+				// Validate merchant_id if present (should be set after OAuth)
+				if (!empty($credentials['merchant_id'])) {
+					if (!is_string($credentials['merchant_id']) ||
+						strlen($credentials['merchant_id']) < 10) {
+						$this->logError('Invalid merchant ID format');
+						return false;
+					}
+				}
+			}
+
+			// Validate environment setting
+			if (isset($credentials['environment'])) {
+				$validEnvironments = ['sandbox', 'production'];
+				if (!in_array($credentials['environment'], $validEnvironments)) {
+					$this->logError('Invalid environment setting', [
+						'provided' => $credentials['environment'],
+						'valid' => $validEnvironments
+					]);
+					return false;
+				}
+			}
+
+			return true;
+		}
+
+		// Fallback for non-OAuth (shouldn't happen for Square, but good practice)
+		return !empty($credentials);
+	}
+
+
+	protected function sanitizeCredentials(array $credentials): array
+	{
+		$sanitized = [];
+
+		// Define which fields should be sanitized and how
+		$text_fields = [
+			'client_id',
+			'client_secret',
+			'access_token',
+			'refresh_token',
+			'merchant_id',
+			'location_id',
+			'webhook_signature_key'
+		];
+
+		foreach ($credentials as $key => $value) {
+			// Handle text fields - trim whitespace but preserve case
+			if (in_array($key, $text_fields)) {
+				if (is_string($value)) {
+					$sanitized[$key] = trim($value);
+				} else {
+					$sanitized[$key] = $value;
+				}
+			}
+			// Handle environment field
+			elseif ($key === 'environment') {
+				$sanitized[$key] = sanitize_key($value);
+			}
+			// Handle boolean fields
+			elseif ($key === 'use_sandbox' || str_starts_with($key, 'enable_')) {
+				$sanitized[$key] = (bool) $value;
+			}
+			// Handle numeric fields
+			elseif ($key === 'expires_at' || $key === 'updated_at' || $key === 'expires_in') {
+				$sanitized[$key] = is_numeric($value) ? (int) $value : $value;
+			}
+			// Handle arrays (like locations)
+			elseif (is_array($value)) {
+				$sanitized[$key] = array_map('sanitize_text_field', $value);
+			}
+			// Default sanitization for other string fields
+			else {
+				if (is_string($value)) {
+					$sanitized[$key] = sanitize_text_field($value);
+				} else {
+					$sanitized[$key] = $value;
+				}
+			}
+		}
+
+		return $sanitized;
+	}
+
+	public function getServiceDescription(): string
+	{
+		return "Connect with Square for payment processing, inventory management, and customer synchronization.";
+	}
+
+	public function setContentTypes(): void
+	{
+		$base = $this->setBaseFields();
+		$this->contentTypes = [
+			'REGULAR'	=> $base,
+			'FOOD_AND_BEV'	=> array_merge($base, $this->setFoodAndBevFields()),
+			'APPOINTMENTS_SERVICE' => array_merge($this->setAppointmentServiceFields()),
+			'EVENT'			=> array_merge($this->setEventFields()),
+			'GIFT_CARD'	=> array_merge($this->setGiftCardFields())
+		];
+	}
+	public function getAdditionalFields(?string $content_type = null):array {
+		if ($content_type === 'customer') {
+			return $this->getCustomerFields();
+		}
+		if ($content_type && array_key_exists($content_type, $this->contentTypes)){
+			$array = $this->contentTypes[$content_type];
+			return array_combine(
+				array_map(fn($k) => '_square_' . $k, array_keys($array)),
+				$array
+			);
+		} else if ($content_type && !array_key_exists($content_type, $this->contentTypes)) {
+			error_log('Could not get default fields for '.$this->service_name.' content type: '.$content_type);
+			return [];
+		}
+		$array = $this->setBaseFields();
+        return array_combine(
+            array_map(fn($k) => '_square_' . $k, array_keys($array)),
+            $array
+        );
+	}
+
+	protected function getCustomerFields():array
+	{
+		return [
+			'customer_id'	=> [
+				'type'	=> 'text',
+				'label'	=> 'Square Customer ID',
+				'hidden'=> true,
+				'section'=> 'your-account'
+			],
+			'address'	=> [
+				'type'	=> 'group',
+				'fields'	=> [
+					'address_line_1'	=> [
+						'type'	=> 'text',
+						'label'	=> 'Address Line 1',
+						'hint'	=> 'ex: 6551 111 St NW',
+						'required'	=> true,
+						'section'	=> 'address'
+					],
+					'address_line_2'	=> [
+						'type'	=> 'text',
+						'label'	=> 'Address Line 2',
+						'hint'	=> 'ex: Unit 2',
+						'section'	=> 'address'
+					],
+					'locality'	=> [
+						'type'	=> 'text',
+						'label'=> 'City',
+						'section'	=> 'address',
+						'required'	=> true,
+					],
+					'administrative_district_level_1' => [
+						'type'	=> 'text',
+						'label' => 'Province',
+						'hint' => 'The two-character code, example: AB',
+						'default'=> 'AB',
+						'section'	=> 'address',
+						'required'	=> true,
+					],
+					'postal_code' => [
+						'type'	=> 'text',
+						'label'	=> 'Postal Code',
+						'section'=> 'address'
+					],
+					'country'	=> [
+						'type'	=> 'text',
+						'label'	=> 'Country Code',
+						'hint'	=> 'The tw-character country code, example: CA',
+						'default'	=> 'CA',
+						'section'	=> 'address',
+						'required'	=> true,
+					],
+					'first_name'	=> [
+						'type'	=> 'text',
+						'label'	=> 'First Name (if different)',
+						'hint'	=> 'Optional field to save the address to someone that isn\'t you'
+					],
+					'last_name'	=> [
+						'type'	=> 'text',
+						'label'	=> 'Last Name (if different)',
+						'hint'	=> 'Optional field to save the address to someone that isn\'t you'
+					]
+				]
+			]
+
+		];
+	}
+	protected function setBaseFields():array
+	{
+		return [
+			'price' => [
+				'type'        => 'number',
+				'label'       => 'Price',
+				'step'        => 0.01,
+				'max'         => 99999,
+				'description' => 'Price for this variation'
+			],
+			'sku' => [
+				'type'        => 'text',
+				'label'       => 'SKU',
+				'description' => 'Stock keeping unit'
+			],
+			'cart_quantity'	=> [
+				'type'	=> 'number',
+				'label'	=> 'Quantity',
+				'hidden'	=> true,
+			],
+			'available_for_pickup' => [
+				'type'	=> 'true_false',
+				'label'	=> 'Available for Pick Up',
+				'section'=> 'square-advanced'
+			],
+			'available_online' => [
+				'type'	=> 'true_false',
+				'label'	=> 'Available online',
+				'section'=> 'square-advanced'
+			],
+			'product_variations' => [
+				'type'        => 'repeater',
+				'label'       => 'Product Variations',
+				'description' => 'Different versions of this product (sizes, colors, etc.)',
+				'add_label'   => 'Add Variation',
+				'section'     => 'variations',
+				'fields'      => $this->setVariationFields()
+			],
+		];
+	}
+
+	protected function setVariationFields(?string $type = null):array
+	{
+		$fields = [
+			'name' => [
+				'type'        => 'text',
+				'label'       => 'Variation Name',
+				'description' => 'e.g., "Small", "Large", "Red", etc.'
+			],
+            'sku' => [
+                'type'        => 'text',
+                'label'       => 'SKU',
+                'description' => 'Stock keeping unit'
+            ],
+			'price' => [
+				'type'        => 'number',
+				'label'       => 'Price',
+				'step'        => 0.01,
+				'max'         => 99999,
+				'description' => 'Price for this variation'
+			],
+			'track_inventory'	=> [
+				'type'	=> 'true_false',
+				'label'	=> 'Track Inventory',
+			],
+			'item_id' => [
+				'type'        => 'text',
+				'label'       => 'Square Variation ID',
+				'description' => 'Square catalog ID for this variation',
+				'hidden'      => true
+			],
+			'last_sync' => [
+				'type'   => 'datetime',
+				'label'  => 'Last Sync',
+				'hidden' => true
+			]
+		];
+
+		switch ($type) {
+			case 'FOOD_AND_BEV':
+				$fields['ingredients']	= [
+					'type'	=> 'textarea',
+					'label'	=> 'Ingredients List',
+					'description'	=> 'Separate ingredients with commas',
+				];
+				$fields['preparation_time_duration']	= [
+					'type'	=> 'number',
+					'label'	=> 'Preparation time (in minutes)',
+				];
+				break;
+			case 'GIFT_CARD':
+				$fields['gift_card_type']	= [
+					'type'	=> 'select',
+					'label'	=> 'Gift Card Type',
+					'options'	=> [
+						'PHYSICAL'	=> 'Physical',
+						'DIGITAL'	=> 'Digital',
+					],
+					'default'	=> 'DIGITAL',
+				];
+				break;
+			case 'APPOINTMENTS_SERVICE':
+				$fields['service_duration']	= [
+					'type'	=> 'number',
+					'label'	=> 'Duration of Service in Minutes'
+				];
+				$fields['available_for_booking']	= [
+					'type'	=> 'true_false',
+					'label'	=> 'Available for Booking'
+				];
+				break;
+		}
+		return array_combine(
+            array_map(fn($k) => '_square_' . $k, array_keys($fields)),
+            $fields
+        );
+	}
+	protected function setFoodAndBevFields():array
+	{
+		return [
+			'ingredients'	=> [
+				'type'	=> 'textarea',
+				'label'	=> 'Ingredients',
+				'hint'	=> 'A comma separated list of ingredients'
+			],
+			'preparation_time_duration' => [
+				'label'	=> 'Preparation Time',
+				'type'	=> 'number',
+				'hint'	=> 'Preparation time in minutes'
+			],
+			'product_variations' => [
+				'type'        => 'repeater',
+				'label'       => 'Product Variations',
+				'description' => 'Different versions of this product (sizes, colors, etc.)',
+				'add_label'   => 'Add Variation',
+				'section'     => 'variations',
+				'fields'      => $this->setVariationFields('FOOD_AND_BEV')
+			],
+		];
+		/* TODO: Set definitions for:
+			'dietary_preferences'	=> [
+
+			],
+			'calories_text'	=> [
+
+			],
+		*/
+	}
+	protected function setAppointmentServiceFields():array
+	{
+		return [
+
+			'product_variations' => [
+				'type'        => 'repeater',
+				'label'       => 'Product Variations',
+				'description' => 'Different versions of this product (sizes, colors, etc.)',
+				'add_label'   => 'Add Variation',
+				'section'     => 'variations',
+				'fields'      => $this->setVariationFields('APPOINTMENTS_SERVICE')
+			],
+		];
+	}
+	protected function setEventFields():array
+	{
+		return [
+			'venue_details'	=> [
+				'type'	=> 'textarea',
+				'label'	=> 'Venue Details',
+			],
+			'capacity'	=> [
+				'type'	=> 'number',
+				'label'	=> 'Capacity'
+			],
+			'product_variations' => [
+				'type'        => 'repeater',
+				'label'       => 'Product Variations',
+				'description' => 'Different versions of this product (sizes, colors, etc.)',
+				'add_label'   => 'Add Variation',
+				'section'     => 'variations',
+				'fields'      => $this->setVariationFields('EVENT')
+			],
+		];
+	}
+	protected function setGiftCardFields():array
+	{
+		return [
+//			'allowed_locations'	=> [
+//
+//			],
+			'product_variations' => [
+				'type'        => 'repeater',
+				'label'       => 'Product Variations',
+				'description' => 'Different versions of this product (sizes, colors, etc.)',
+				'add_label'   => 'Add Variation',
+				'section'     => 'variations',
+				'fields'      => $this->setVariationFields('GIFT_CARD')
+			],
+		];
+	}
+
+	/*********************************************************
+	IMAGE PROCESSING
+	 *********************************************************/
+	/**
+	 * Upload image file to Square
+	 *
+	 * @param int $imgID WordPress attachment ID
+	 * @return array|WP_Error Upload result or error
+	 */
+	protected function uploadImageToSquare(int $imgID): array|WP_Error
+	{
+		$supported_image_id = $this->getSupportedImage($imgID);
+
+		// Check if already uploaded
+		$existing_square_image_id = $this->getSquareImageId($supported_image_id);
+		if ($existing_square_image_id) {
+			return [
+				'success' => true,
+				'image_id' => $existing_square_image_id
+			];
+		}
+
+		$file_path = get_attached_file($supported_image_id);
+		if (!file_exists($file_path)) {
+			return new WP_Error('file_not_found', 'Image file not found');
+		}
+
+		// Verify file type
+		$mime_type = get_post_mime_type($supported_image_id);
+		if (!in_array($mime_type, ['image/jpeg', 'image/png', 'image/gif'])) {
+			return new WP_Error('invalid_type', 'Square only supports JPEG, PNG, and GIF images');
+		}
+
+		$image_title = get_the_title($supported_image_id);
+		$alt_text = get_post_meta($supported_image_id, '_wp_attachment_image_alt', true);
+
+		// Build multipart request - SINGLE STEP
+		$boundary = wp_generate_password(24);
+		$headers = $this->getRequestHeaders();
+		$headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
+
+		// Request JSON part
+		$request_json = [
+			'idempotency_key' => wp_generate_uuid4(),
+			'image' => [
+				'type' => 'IMAGE',
+				'id' => '#IMAGE_' . $supported_image_id . '_' . time(),
+				'image_data' => [
+					'name' => $image_title ?: 'Image',
+					'caption' => $alt_text ?: ''
+				]
+			],
+			'object_id'	=> $supported_image_id
+		];
+
+		$body = $this->buildMultipartBody($file_path, $request_json, $boundary);
+
+		$response = wp_remote_post(
+			$this->getApiUrl('v2/catalog/images'),
+			[
+				'headers' => $headers,
+				'body' => $body,
+				'timeout' => 60
+			]
+		);
+
+		if (is_wp_error($response)) {
+			return $response;
+		}
+
+		$data = json_decode(wp_remote_retrieve_body($response), true);
+
+		if (!empty($data['errors'])) {
+			return new WP_Error('upload_error', $data['errors'][0]['detail'] ?? 'Unknown error');
+		}
+
+		if (!empty($data['image']['id'])) {
+			$this->setSquareImageId($supported_image_id, $data['image']['id']);
+			return [
+				'success' => true,
+				'image_id' => $data['image']['id']
+			];
+		}
+
+		return new WP_Error('upload_failed', 'Failed to upload image');
+	}
+
+	protected function buildMultipartBody(string $file_path, array $request_json, string $boundary): string
+	{
+		$eol = "\r\n";
+		$body = '';
+
+		// Add request JSON part
+		$body .= '--' . $boundary . $eol;
+		$body .= 'Content-Disposition: form-data; name="request"' . $eol;
+		$body .= 'Content-Type: application/json' . $eol . $eol;
+		$body .= json_encode($request_json) . $eol;
+
+		// Add image file part
+		$filename = basename($file_path);
+		$file_contents = file_get_contents($file_path);
+		$mime_type = mime_content_type($file_path);
+
+		$body .= '--' . $boundary . $eol;
+		$body .= 'Content-Disposition: form-data; name="file"; filename="' . $filename . '"' . $eol;
+		$body .= 'Content-Type: ' . $mime_type . $eol . $eol;
+		$body .= $file_contents . $eol;
+		$body .= '--' . $boundary . '--' . $eol;
+
+		return $body;
+	}
+
+	/**
+	 * Attach image to catalog item
+	 *
+	 * @param int $postID WordPress post ID
+	 * @param string $square_image_id Square image ID
+	 * @return array|WP_Error Result or error
+	 */
+	public function attachImageToCatalogItem(int $postID, string $square_image_id): array|WP_Error
+	{
+		$square_catalog_id = get_post_meta($postID, BASE . '_square_catalog_id', true);
+
+		if (!$square_catalog_id) {
+			return new WP_Error('no_catalog_id', 'Post has not been synced to Square');
+		}
+
+		// Get the current catalog item
+		$response = $this->getRequest('catalog/object/' . $square_catalog_id);
+
+		if (is_wp_error($response)) {
+			return $response;
+		}
+
+		if (empty($response['object'])) {
+			return new WP_Error('item_not_found', 'Catalog item not found in Square');
+		}
+
+		$catalog_object = $response['object'];
+
+		// Add image to item data
+		$catalog_object['item_data']['image_ids'] = [$square_image_id];
+
+		// Update the catalog item
+		$update_response = $this->postRequest('catalog/batch-upsert', [
+			'idempotency_key' => wp_generate_uuid4(),
+			'batches' => [[
+				'objects' => [$catalog_object]
+			]]
+		]);
+
+		if (is_wp_error($update_response)) {
+			return $update_response;
+		}
+
+		return [
+			'success' => true,
+			'message' => 'Image attached to catalog item'
+		];
+	}
+
+	/**
+	 * Get stored Square image ID for an attachment
+	 *
+	 * @param int $attachment_id WordPress attachment ID
+	 * @return string|false Square image ID or false if not found
+	 */
+	public function getSquareImageId(int $attachment_id): string|false
+	{
+		$image_id = get_post_meta($attachment_id, BASE . 'square_image_id', true);
+
+		if ($image_id) {
+			// Verify it still exists in Square
+			return $this->verifySquareImage($image_id) ? $image_id : false;
+		}
+
+		return false;
+	}
+
+	/**
+	 * Store Square image ID for an attachment
+	 *
+	 * @param int $attachment_id WordPress attachment ID
+	 * @param string $square_image_id Square image ID
+	 * @return bool Success
+	 */
+	public function setSquareImageId(int $attachment_id, string $square_image_id): bool
+	{
+		return update_post_meta($attachment_id, BASE . 'square_image_id', $square_image_id);
+	}
+
+	/**
+	 * Verify image exists in Square catalog
+	 *
+	 * @param string $square_image_id Square image ID
+	 * @return bool Whether image exists
+	 */
+	protected function verifySquareImage(string $square_image_id): bool
+	{
+		$response = $this->getRequest('catalog/object/' . $square_image_id);
+
+		if (is_wp_error($response)) {
+			return false;
+		}
+
+		return !empty($response['object']) && $response['object']['type'] === 'IMAGE';
+	}
+
+	/**
+	 * Batch upload images for multiple posts
+	 *
+	 * @param array $post_ids Array of WordPress post IDs
+	 * @return array Results for each post
+	 */
+	private function batchUploadImages(array $post_ids): array
+	{
+		$image_mappings = [];
+		$image_objects = [];
+
+		// First pass: collect images that need uploading
+		foreach ($post_ids as $post_id) {
+			$thumbnail_id = get_post_thumbnail_id($post_id);
+			if (!$thumbnail_id) {
+				$image_mappings[$post_id] = null;
+				continue;
+			}
+
+			// Check if already uploaded and still exists in Square
+			$existing_square_id = get_post_meta($thumbnail_id, BASE . '_square_image_id', true);
+			if ($existing_square_id && $this->verifySquareImage($existing_square_id)) {
+				$image_mappings[$post_id] = $existing_square_id;
+				continue;
+			}
+
+			// Get supported image format (handles WebP conversion)
+			$supported_image_id = $this->getSupportedImage($thumbnail_id);
+			if (!$supported_image_id) {
+				$image_mappings[$post_id] = null;
+				continue;
+			}
+
+			// Upload the image to Square using the proper endpoint
+			$square_image_id = $this->uploadSingleImageToSquare($supported_image_id, $post_id);
+
+			if ($square_image_id && !is_wp_error($square_image_id)) {
+				$image_mappings[$post_id] = $square_image_id;
+				// Store the Square image ID for future reference
+				update_post_meta($thumbnail_id, BASE . '_square_image_id', $square_image_id);
+			} else {
+				$this->logError('Failed to upload image for post', [
+					'post_id' => $post_id,
+					'thumbnail_id' => $thumbnail_id,
+					'error' => is_wp_error($square_image_id) ? $square_image_id->get_error_message() : 'Unknown error'
+				]);
+				$image_mappings[$post_id] = null;
+			}
+		}
+
+		return $image_mappings;
+	}
+
+	/**
+	 * Upload a single image to Square using the CreateCatalogImage endpoint
+	 *
+	 * @param int $attachment_id WordPress attachment ID
+	 * @param int $post_id Associated post ID
+	 * @return string|WP_Error Square image ID or error
+	 */
+	private function uploadSingleImageToSquare(int $attachment_id, int $post_id): string|WP_Error
+	{
+		$file_path = get_attached_file($attachment_id);
+
+		if (!file_exists($file_path)) {
+			return new WP_Error('file_not_found', 'Image file not found');
+		}
+
+		// Verify file type
+		$mime_type = get_post_mime_type($attachment_id);
+		$allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
+
+		if (!in_array($mime_type, $allowed_types)) {
+			return new WP_Error('invalid_type', 'Square only supports JPEG, PNG, and GIF images');
+		}
+
+		// Prepare multipart form data
+		$boundary = wp_generate_password(24);
+		$headers = $this->getRequestHeaders();
+		$headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
+
+		// Build the request body
+		$eol = "\r\n";
+		$body = '';
+
+		// Add request JSON
+		$request_data = [
+			'idempotency_key' => wp_generate_uuid4(),
+			'image' => [
+				'type' => 'IMAGE',
+				'id' => '#TEMP_IMAGE_' . $attachment_id . '_' . time(),
+				'image_data' => [
+					'name' => get_the_title($attachment_id) ?: basename($file_path),
+					'caption' => get_post_meta($attachment_id, '_wp_attachment_image_alt', true) ?:
+						sprintf('Image for %s', get_the_title($post_id))
+				]
+			]
+		];
+
+		$body .= '--' . $boundary . $eol;
+		$body .= 'Content-Disposition: form-data; name="request"' . $eol;
+		$body .= 'Content-Type: application/json' . $eol . $eol;
+		$body .= json_encode($request_data) . $eol;
+
+		// Add image file
+		$filename = basename($file_path);
+		$file_contents = file_get_contents($file_path);
+
+		$body .= '--' . $boundary . $eol;
+		$body .= 'Content-Disposition: form-data; name="image"; filename="' . $filename . '"' . $eol;
+		$body .= 'Content-Type: ' . $mime_type . $eol . $eol;
+		$body .= $file_contents . $eol;
+
+		// End boundary
+		$body .= '--' . $boundary . '--' . $eol;
+
+		// Make the request to the CreateCatalogImage endpoint
+		$response = wp_remote_post(
+			$this->getApiUrl('catalog/images'),
+			[
+				'headers' => $headers,
+				'body' => $body,
+				'timeout' => 60
+			]
+		);
+
+		if (is_wp_error($response)) {
+			return $response;
+		}
+
+		$response_body = wp_remote_retrieve_body($response);
+		$data = json_decode($response_body, true);
+
+		if (!empty($data['errors'])) {
+			$error_message = $data['errors'][0]['detail'] ?? 'Unknown error';
+			return new WP_Error('upload_error', $error_message);
+		}
+
+		if (!empty($data['image']['id'])) {
+			$this->logDebug('Successfully uploaded image to Square', [
+				'post_id' => $post_id,
+				'attachment_id' => $attachment_id,
+				'square_image_id' => $data['image']['id']
+			]);
+			return $data['image']['id'];
+		}
+
+		return new WP_Error('upload_failed', 'Failed to get image ID from Square response');
+	}
+
+	protected function handleApiError(int $code, string $body, string $endpoint): void
+	{
+		parent::handleApiError($code, $body, $endpoint);
+
+		$decoded = json_decode($body, true);
+		$errorCode = $decoded['errors'][0]['code'] ?? '';
+
+		// Handle Square-specific OAuth errors
+		if ($errorCode === 'ACCESS_TOKEN_EXPIRED') {
+			$this->logDebug('Access token expired, attempting refresh');
+			if ($this->refreshOAuthToken()) {
+				// Token refreshed, could retry the request
+				$this->logDebug('Token refreshed successfully');
+			}
+		} elseif ($errorCode === 'UNAUTHORIZED' || $errorCode === 'ACCESS_TOKEN_REVOKED') {
+			$this->logError('Square authorization revoked or invalid', [
+				'error_code' => $errorCode,
+				'user_id' => $this->userID
+			]);
+			// Clear invalid credentials
+			$this->deleteCredentials();
+		}
+	}
+
+	public function createSquareOrder(array $items, ?string $customer_id, array $data): array|WP_Error
+	{
+		// Build line items for Square
+		$line_items = [];
+		foreach ($items as $item) {
+			$line_item = [
+				'quantity' => (string)$item['quantity'], // MUST be string!
+			];
+
+			// Use catalog_object_id if available (recommended)
+			if (!empty($item['catalog_object_id'])) {
+				$line_item['catalog_object_id'] = $item['catalog_object_id'];
+				$line_item['catalog_version'] = $item['catalog_version'] ?? null;
+			} else {
+				// Ad-hoc line item (not recommended - no tax/inventory automation)
+				$line_item['name'] = $item['name'];
+				$line_item['base_price_money'] = [
+					'amount' => (int)$item['_square_price'],
+					'currency' => $this->getCurrency()
+				];
+			}
+
+			if (!empty($item['note'])) {
+				$line_item['note'] = $item['note'];
+			}
+
+			$line_items[] = $line_item;
+		}
+
+		$order_data = [
+			'idempotency_key' => wp_generate_uuid4(), // Different from payment idempotency key
+			'order' => [
+				'location_id' => $this->locationId,
+				'line_items' => $line_items,
+				'state' => 'OPEN'
+			]
+		];
+
+		// Add customer if available
+		if ($customer_id) {
+			$order_data['order']['customer_id'] = $customer_id;
+		}
+
+		// Add metadata
+		if (!empty($data['note'])) {
+			$order_data['order']['metadata'] = [
+				'special_instructions' => $data['note']
+			];
+		}
+
+		if (!empty($data['pickup_time'])) {
+			$order_data['order']['metadata']['pickup_time'] = $data['pickup_time'];
+		}
+
+		return $this->postRequest('orders', $order_data);
+	}
+
+	public function createSquarePayment(
+		string $source_id,
+		string $idempotency_key,
+		int $amount_cents,
+		string $order_id,
+		?string $customer_id
+	): array|WP_Error
+	{
+		$payment_data = [
+			'idempotency_key' => $idempotency_key,
+			'source_id' => $source_id,
+			'amount_money' => [
+				'amount' => $amount_cents, // Already in cents!
+				'currency' => $this->getCurrency()
+			],
+			'order_id' => $order_id,
+			'location_id' => $this->locationId,
+			'autocomplete' => true, // Capture immediately
+		];
+
+		// Add customer if available
+		if ($customer_id) {
+			$payment_data['customer_id'] = $customer_id;
+		}
+
+		// Add reference ID for tracking
+		$payment_data['reference_id'] = 'WP_' . time();
+
+		return $this->postRequest('payments', $payment_data);
+	}
+
+	public function saveOrderToWordPress(array $order_data): int
+	{
+		// Extract customer info
+		$customer_email = $order_data['customer']['email'] ?? '';
+		$customer_name = $order_data['customer']['name'] ?? '';
+
+		// Find or create WP user for logged-in association
+		$user_id = 0;
+		if ($customer_email) {
+			$user = get_user_by('email', $customer_email);
+			if ($user) {
+				$user_id = $user->ID;
+				// Store Square customer ID on user
+				if (!empty($order_data['square_customer_id'])) {
+					update_user_meta($user_id, BASE . '_square_customer_id', $order_data['square_customer_id']);
+				}
+			}
+		}
+
+		// Create order post
+		$order_post_id = wp_insert_post([
+			'post_type' => BASE.$this->orderPostType,
+			'post_title' => 'Order #' . $order_data['square_order_id'],
+			'post_status' => 'publish',
+			'post_author' => $user_id // Associate with user if logged in
+		]);
+
+		if (!$order_post_id || is_wp_error($order_post_id)) {
+			$this->logError('Failed to create order post', ['order_data' => $order_data]);
+			return 0;
+		}
+
+		// Save all order meta
+		$meta = Meta::forPost($order_post_id);
+		$fields = $this->getOrderFields();
+		unset($fields['post_title']);
+
+		$meta->setAll([
+			'square_order_id' => $order_data['square_order_id'],
+			'square_payment_id' => $order_data['square_payment_id'] ?? '',
+			'square_customer_id' => $order_data['square_customer_id'] ?? '',
+			'amount' => $order_data['amount'],
+			'status' => $order_data['status'],
+			'fulfillment_status' => $order_data['fulfillment_status'] ?? 'PROPOSED',
+			'pickup_time' => $order_data['pickup_time'] ?? '',
+			'customer_email' => $customer_email,
+			'customer_name' => $customer_name,
+			'customer_phone' => $order_data['customer']['phone'] ?? '',
+			'special_instructions' => $order_data['note'] ?? '',
+			'items' => $order_data['items'],
+			'receipt_url' => $order_data['receipt_url'] ?? '',
+			'created_at' => current_time('mysql'),
+			'updated_at' => current_time('mysql')
+		]);
+
+		return $order_post_id;
+	}
+
+	/**
+	 * Get currency code
+	 */
+	private function getCurrency(): string
+	{
+		return get_option(BASE . 'currency', 'CAD');
+	}
+
+	/**
+	 * Get customer with saved cards (2025-compliant)
+	 */
+	public function getUserCards(string $customer_id): array
+	{
+		$response = $this->getRequest('cards?customer_id=' . $customer_id);
+		return $response['cards'] ?? [];
+	}
+
+
+	public function getUserOrders(string $customer_email): array
+	{
+		// First get Square customer ID
+		$customer_response = $this->postRequest('customers/search', [
+			'filter' => [
+				'email_address' => ['exact' => $customer_email]
+			]
+		]);
+
+		$customer_id = $customer_response['customers'][0]['id'] ?? null;
+		if (!$customer_id) {
+			return [];
+		}
+
+		// Get their orders
+		$orders_response = $this->postRequest('orders/search', [
+			'filter' => [
+				'customer_filter' => [
+					'customer_ids' => [$customer_id]
+				]
+			],
+			'sort' => [
+				'sort_field' => 'CREATED_AT',
+				'sort_order' => 'DESC'
+			],
+			'limit' => 50
+		]);
+
+		return $orders_response['orders'] ?? [];
+	}
+
+	public function checkOrderStatus(string $order_id): ?string
+	{
+		// Fetch from Square
+		$response = $this->getRequest('orders/' . $order_id);
+		if (!is_wp_error($response)) {
+			return $response['order']['state'] ?? null;
+		}
+
+		return null;
+	}
+
+	/**
+	 * Single-item sync. Called by IntegrationExecutor::processSyncTo().
+	 * Delegates to syncBatchToService since Square uses batch-upsert.
+	 */
+	public function syncPostToService(int $postID): array|WP_Error
+	{
+		return $this->syncBatchToService(['items' => [$postID]]);
+	}
+
+	/**
+	 * Batch sync — preferred by IntegrationExecutor when available.
+	 * Wraps existing processSyncToSquare which already handles batches.
+	 */
+	public function syncBatchToService(array $data): array|WP_Error
+	{
+		$result = $this->processSyncToSquare($data);
+
+		if (empty($result['success'])) {
+			$errors = implode(', ', $result['result']['errors'] ?? ['Sync failed']);
+			return new WP_Error('square_sync_failed', $errors);
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Delete catalog object from Square.
+	 * Called by IntegrationExecutor::processDeleteFrom().
+	 */
+	public function deleteFromService(string $externalId): array|WP_Error
+	{
+		$result = $this->processDeleteFromSquare(['square_ids' => [$externalId]]);
+
+		if (empty($result['success'])) {
+			return new WP_Error('square_delete_failed', $result['result']['error'] ?? 'Delete failed');
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Import from Square catalog → WordPress.
+	 * Called by IntegrationExecutor::processImport().
+	 */
+	public function importFromService(array $data): array|WP_Error
+	{
+		$result = $this->processSyncFromSquare($data);
+
+		if (empty($result['success'])) {
+			return new WP_Error('square_import_failed', $result['result']['error'] ?? 'Import failed');
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Sync customer to Square.
+	 * Called by IntegrationExecutor::processSyncCustomer().
+	 */
+	public function syncCustomer(array $data): array|WP_Error
+	{
+		$result = $this->processSyncCustomer($data);
+
+		if (empty($result['success'])) {
+			return new WP_Error('square_customer_sync_failed', $result['result']['error'] ?? 'Customer sync failed');
+		}
+
+		return $result;
+	}
+
+	public function addDashboardPages():void
+	{
+		$page = JVB()->dashboard()->addPage('Your Orders', 'orders', 'receipt');
+		$page->setScripts(['jvb-crud']);
+	}
+	public function renderDashPage():void
+	{
+		$this->determineUser();
+
+		$crud = new CRUDSkeleton();
+		$crud->icon('receipt');
+		$crud->title('Your Orders','Here you can see your past orders, and reorder from there if you\'d like');
+		$crud->content($this->orderPostType, 'Order', 'Orders');
+		$crud->addSearch();
+		$crud->addCapabilities(['view']);
+		$crud->setEmptyState(sprintf(
+			'<div class="empty-state">
+				<h3>%sNothing here%s</h3>
+				<p>It doesn\'t look like you have any orders yet.</p>
+				<p>Head on over to <a href="%s">our menu</a> and make your first order!</p>
+			</div>',
+			jvbDashIcon($crud->getIcon()),
+			jvbDashIcon($crud->getIcon()),
+			get_post_type_archive_link(BASE.'menu_item')
+		));
+
+
+		$crud->render();
+	}
+
+
+	public function getOrderPost(string $order_id):int|false
+	{
+		$posts = new WP_Query([
+			'post_type'			=> BASE.$this->orderPostType,
+			'posts_per_page'	=> 1,
+			'meta_key'			=> BASE.'square_order_id',
+			'meta_value'		=> $order_id,
+			'fields'			=> 'ids',
+		]);
+		wp_reset_postdata();
+		return $posts->have_posts() ? $posts[0] : false;
+	}
+	public function getOrderHistory(int $user_id):array
+	{
+		$posts = new WP_Query([
+			'post_type'			=> BASE.$this->orderPostType,
+			'posts_per_page'	=> 25,
+			'author'			=> $user_id,
+			'orderby'			=> 'date',
+			'order'				=> 'desc',
+			'fields'			=> 'ids',
+		]);
+		wp_reset_postdata();
+		return array_map(function ($post) {
+			$fields = Meta::forPost($post);
+			$fields['wp_order_id'] = $post;
+			return $fields;
+		}, $posts->posts);
+
+	}
+
+	public function formatPrice(string|float|int $priceValue):int
+	{
+		//Convert dollars to cents
+		return intval(floatval($priceValue) * 100);
+	}
+}
diff --git a/inc/integrations/services/UmamiOld.php b/inc/integrations/services/UmamiOld.php
new file mode 100644
index 0000000..5c02071
--- /dev/null
+++ b/inc/integrations/services/UmamiOld.php
@@ -0,0 +1,803 @@
+<?php
+namespace JVBase\integrations\services;
+use JVBase\integrations\Integrations;
+
+if (!defined('ABSPATH')) {
+	exit;
+}
+
+class UmamiOld extends Integrations
+{
+	private string $website_id;
+	private string $api_url;
+	private string $api_token;
+
+	// Tracking configuration
+	private array $valid_events = [
+		'view_feed',
+		'view_taxonomy',
+		'view_profile',
+		'view_shop',
+		'view_content',
+		'toggle_favourite',
+		'click_profile',
+		'click_content',
+		'click_taxonomy',
+		'click_shop',
+		'user_session'
+	];
+
+	// Database tables
+	private string $events_table;
+	private string $metrics_table;
+
+	protected static array $instances = [];
+	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);
+		}
+		return self::$instances[$key];
+	}
+
+	protected function __construct(?int $userID = null)
+	{
+		$this->service_name = 'umami';
+		$this->title = 'Umami.js';
+		$this->icon = 'chart-line';
+		$this->apiBase = '';  // Set dynamically based on credentials
+		$this->apiEndpoints = [
+			'api/websites',
+			'api/websites/{websiteId}/stats',
+			'api/websites/{websiteId}/pageviews',
+			'api/websites/{websiteId}/events'
+		];
+
+		$this->fields = [
+			'website_id' => [
+				'label'		=> 'Website ID',
+				'type'		=> 'text',
+				'placeholder'=> 'Enter Umami Website ID',
+				'hint'		=> 'Found in your Umami dashboard under Settings → Websites.',
+				'required'	=> true
+			],
+		];
+
+		$this->advanced = [
+			'api_url' => [
+				'label'		=> 'API URL',
+				'type'		=> 'text',
+				'placeholder'=> 'https://analytics.yourdomain.com',
+				'hint'		=> 'Your Umami server URL. Leave blank if using umami.is cloud service.'
+			],
+			'api_token' => [
+				'label'		=> 'API Token (Optional)',
+				'type'		=> 'text',
+				'subtype'	=> 'password',
+				'placeholder'=> 'Enter API Token for analytics data',
+				'hint'		=> 'Required only if you want to retrieve analytics data for dashboards.'
+			]
+		];
+
+		$this->instructions = [
+			'Login to your <a href="https://cloud.umami.is/settings/websites" target="_blank">umami.is</a> account, select your website, and copy the website ID'
+		];
+
+		parent::__construct();
+
+		$this->actions = array_merge(
+			$this->actions,
+			[
+				'refresh_data'	=> 	'handleRefreshData'
+			]
+		);
+
+		global $wpdb;
+		$this->events_table = $wpdb->prefix . BASE . 'umami_events';
+		$this->metrics_table = $wpdb->prefix . BASE . 'performance_metrics';
+	}
+
+	/**
+	 * Initialize service-specific properties
+	 */
+	protected function initialize(): void
+	{
+		$this->website_id = $this->credentials['website_id'] ?? '';
+		$this->api_url = $this->credentials['api_url'] ?? '';
+		$this->api_token = $this->credentials['api_token'] ?? '';
+		$this->ttl = intval($this->credentials['cache_duration'] ?? 60) * 60;
+
+		// Set dynamic API base if provided
+		if (!empty($this->api_url)) {
+			$this->apiBase = rtrim($this->api_url, '/');
+		}
+	}
+
+	/**
+	 * Register additional WordPress hooks
+	 */
+	protected function registerAdditionalHooks(): void
+	{
+		// Add tracking script to frontend
+		add_action('wp_head', [$this, 'renderTrackingScript'], 5);
+
+		// Add user session tracking
+		add_action('wp_footer', [$this, 'trackUserSession'], 100);
+
+		// Schedule data collection if API is configured
+		if (!empty($this->api_token)) {
+			add_action('jvb_daily_umami_collection', [$this, 'collectDailyData']);
+
+			if (!wp_next_scheduled('jvb_daily_umami_collection')) {
+				wp_schedule_event(time(), 'daily', 'jvb_daily_umami_collection');
+			}
+		}
+	}
+
+	/***************************************************************************
+	 * TRACKING SCRIPT METHODS
+	 ***************************************************************************/
+
+	/**
+	 * Render tracking script in head
+	 */
+	public function renderTrackingScript(): void
+	{
+		// Skip on local environments
+		if (JVB_TESTING) {
+			return;
+		}
+		if (!$this->isSetUp() || is_admin()) {
+			return;
+		}
+
+
+		$script_url = $this->getTrackingScriptUrl();
+		$website_id = $this->getWebsiteId();
+
+		if (!$website_id || !$script_url) {
+			return;
+		}
+
+		// Preconnect for performance
+		$domain = parse_url($script_url, PHP_URL_HOST);
+		echo '<link rel="preconnect" href="https://' . esc_attr($domain) . '"/>' . "\n";
+
+		// Add tracking script
+		$attributes = [
+			'defer',
+			'src="' . esc_url($script_url) . '"',
+			'data-website-id="' . esc_attr($website_id) . '"'
+		];
+
+		// Add optional configuration
+		if (!empty($this->credentials['respect_dnt']) && $this->credentials['respect_dnt']) {
+			$attributes[] = 'data-do-not-track="true"';
+		}
+
+		if (!empty($this->credentials['domains'])) {
+			$attributes[] = 'data-domains="' . esc_attr($this->credentials['domains']) . '"';
+		}
+
+		echo '<script ' . implode(' ', $attributes) . '></script>' . "\n";
+	}
+
+	/**
+	 * Track user session information
+	 */
+	public function trackUserSession(): void
+	{
+		if (!$this->isSetUp() || is_admin()) {
+			return;
+		}
+
+		$attributes = $this->buildTrackingAttributes('user_session', 'pageview', [
+			'logged_in' => is_user_logged_in() ? 'true' : 'false',
+			'user_type' => $this->getCurrentUserType()
+		]);
+
+		if (empty($attributes)) {
+			return;
+		}
+
+		echo '<div ' . $this->attributesToString($attributes) . ' style="display:none"></div>' . "\n";
+
+		// Add page-specific tracking
+		$this->trackPageView();
+	}
+
+	/**
+	 * Track specific page views
+	 */
+	private function trackPageView(): void
+	{
+		$tracker_args = [];
+		$event = '';
+		$type = '';
+
+		if (is_singular([BASE . 'artist', BASE . 'partner'])) {
+			// Profile view
+			global $post;
+			$event = 'view_profile';
+			$type = get_post_type();
+			$tracker_args = [
+				'id' => get_the_ID(),
+				'from' => $this->getTrafficSource(),
+				'owner_id' => $post->post_author
+			];
+
+			// Check for specific content in query
+			foreach (['tattoo', 'piercing', 'artwork'] as $content_type) {
+				if (isset($_GET[$content_type])) {
+					$tracker_args['item'] = sanitize_text_field($_GET[$content_type]);
+					break;
+				}
+			}
+
+		} elseif (is_tax(BASE . 'shop')) {
+			// Shop view
+			$event = 'view_shop';
+			$type = BASE . 'shop';
+			$tracker_args = [
+				'id' => get_queried_object_id(),
+				'from' => $this->getTrafficSource()
+			];
+
+		} elseif (is_tax()) {
+			// Taxonomy view
+			$obj = get_queried_object();
+			$event = 'view_taxonomy';
+			$type = $obj->taxonomy;
+			$tracker_args = [
+				'id' => $obj->term_id,
+				'from' => $this->getTrafficSource()
+			];
+		}
+
+		if ($event && $type) {
+			$attributes = $this->buildTrackingAttributes($event, $type, $tracker_args);
+			echo '<div ' . $this->attributesToString($attributes) . ' style="display:none"></div>' . "\n";
+		}
+	}
+
+	/***************************************************************************
+	 * TRACKING ATTRIBUTE BUILDER METHODS
+	 ***************************************************************************/
+
+	/**
+	 * Build tracking attributes for events
+	 */
+	public function buildTrackingAttributes(string $event, string $type, array $args = []): array
+	{
+		if (!in_array($event, $this->valid_events)) {
+			return [];
+		}
+
+		// Normalize type
+		$normalized_type = str_replace(BASE, '', $type);
+
+		// Build base attributes
+		$attributes = [
+			'data-umami-event' => esc_attr($event),
+			'data-umami-event-type' => esc_attr($normalized_type)
+		];
+
+		// Add ID if provided
+		if (!empty($args['id'])) {
+			$attributes['data-umami-event-id'] = esc_attr($args['id']);
+		}
+
+		// Add source information
+		if (!empty($args['source_id'])) {
+			$attributes['data-umami-event-source'] = esc_attr($args['source_id']);
+			if (!empty($args['source_type'])) {
+				$attributes['data-umami-event-source-type'] = esc_attr(
+					str_replace(BASE, '', $args['source_type'])
+				);
+			}
+		}
+
+		// Add owner information
+		if (!empty($args['owner_id'])) {
+			$attributes['data-umami-event-owner'] = esc_attr($args['owner_id']);
+			if (!empty($args['owner_type'])) {
+				$attributes['data-umami-event-owner-type'] = esc_attr(
+					str_replace(BASE, '', $args['owner_type'])
+				);
+			}
+		}
+
+		// Add traffic source
+		if (!empty($args['from'])) {
+			$attributes['data-umami-event-from'] = esc_attr($args['from']);
+		}
+
+		// Add item reference
+		if (!empty($args['item'])) {
+			$attributes['data-umami-event-item'] = esc_attr($args['item']);
+		}
+
+		// Add metadata
+		foreach ($args as $key => $value) {
+			if (in_array($key, ['id', 'source_id', 'source_type', 'owner_id', 'owner_type', 'from', 'item'])) {
+				continue;
+			}
+			$clean_key = 'data-umami-event-' . str_replace('_', '-', sanitize_key($key));
+			$attributes[$clean_key] = esc_attr($value);
+		}
+
+		return $attributes;
+	}
+
+	/**
+	 * Convert attributes array to HTML string
+	 */
+	public function attributesToString(array $attributes): string
+	{
+		$attr_strings = [];
+		foreach ($attributes as $key => $value) {
+			$attr_strings[] = $key . '="' . $value . '"';
+		}
+		return implode(' ', $attr_strings);
+	}
+
+	/**
+	 * Public tracking methods for use in templates
+	 */
+	public function trackContentClick(int $id, string $type, array $args = []): string
+	{
+		$args['id'] = $id;
+
+		// Auto-detect owner for content types
+		if (empty($args['owner_id']) && in_array($type, [BASE . 'tattoo', BASE . 'artwork', BASE . 'piercing'])) {
+			$post = get_post($id);
+			if ($post && !empty($post->post_author)) {
+				$args['owner_id'] = $post->post_author;
+				$args['owner_type'] = 'user';
+			}
+		}
+
+		return $this->attributesToString(
+			$this->buildTrackingAttributes('click_content', $type, $args)
+		);
+	}
+
+	public function trackProfileClick(int $id, string $type, array $args = []): string
+	{
+		$args['id'] = $id;
+		return $this->attributesToString(
+			$this->buildTrackingAttributes('click_profile', $type, $args)
+		);
+	}
+
+	public function trackTaxonomyClick(int $id, string $taxonomy, array $args = []): string
+	{
+		$args['id'] = $id;
+		return $this->attributesToString(
+			$this->buildTrackingAttributes('click_taxonomy', $taxonomy, $args)
+		);
+	}
+
+	public function trackContentTaxonomyClick(int $id, string $taxonomy, array $args = []): string
+	{
+		$args['id'] = $id;
+		return $this->attributesToString(
+			$this->buildTrackingAttributes('click_' . $taxonomy, $taxonomy, $args)
+		);
+	}
+
+	public function trackFavouriteToggle(int $id, string $type, bool $is_favourite, array $args = []): string
+	{
+		$args['id'] = $id;
+		$args['action'] = $is_favourite ? 'add' : 'remove';
+		return $this->attributesToString(
+			$this->buildTrackingAttributes('toggle_favourite', $type, $args)
+		);
+	}
+
+	public function trackFeedView(int $id, string $type, array $args = []): array
+	{
+		$args['id'] = $id;
+		return $this->buildTrackingAttributes('view_feed', $type, $args);
+	}
+
+	/***************************************************************************
+	 * API DATA COLLECTION METHODS
+	 ***************************************************************************/
+
+	/**
+	 * Get analytics data from Umami API
+	 */
+	public function getAnalytics(string $start_date, string $end_date, bool $use_cache = true): ?array
+	{
+		if (empty($this->api_url) || empty($this->api_token)) {
+			return null;
+		}
+
+		// Check cache first
+		if ($use_cache && $this->cache) {
+			$cache_key = md5("analytics_{$start_date}_{$end_date}");
+			$cached = $this->cache->get($cache_key);
+			if ($cached !== false) {
+				return $cached;
+			}
+		}
+
+		try {
+			$url = rtrim($this->api_url, '/') . "/api/websites/{$this->website_id}/stats";
+			$params = [
+				'startAt' => strtotime($start_date) * 1000,
+				'endAt' => strtotime($end_date) * 1000
+			];
+
+			$response = wp_remote_get($url . '?' . http_build_query($params), [
+				'headers' => [
+					'Authorization' => 'Bearer ' . $this->api_token,
+					'Accept' => 'application/json'
+				],
+				'timeout' => 30
+			]);
+
+			if (is_wp_error($response)) {
+				$this->logError('API request failed', ['error' => $response->get_error_message()]);
+				return null;
+			}
+
+			$body = wp_remote_retrieve_body($response);
+			$data = json_decode($body, true);
+
+			if (json_last_error() !== JSON_ERROR_NONE) {
+				$this->logError('Invalid JSON response from API');
+				return null;
+			}
+
+			// Cache the result
+			if ($use_cache && $this->cache) {
+				$this->cache->set($cache_key, $data, $this->ttl);
+			}
+
+			return $data;
+
+		} catch (Exception $e) {
+			$this->logError('Exception during API call', ['error' => $e->getMessage()]);
+			return null;
+		}
+	}
+
+	/**
+	 * Get page views for a specific URL
+	 */
+	public function getPageViews(string $url, string $start_date, string $end_date): ?array
+	{
+		if (empty($this->api_url) || empty($this->api_token)) {
+			return null;
+		}
+
+		$api_endpoint = rtrim($this->api_url, '/') . "/api/websites/{$this->website_id}/pageviews";
+		$params = [
+			'startAt' => strtotime($start_date) * 1000,
+			'endAt' => strtotime($end_date) * 1000,
+			'url' => $url,
+			'unit' => 'day'
+		];
+
+		$response = wp_remote_get($api_endpoint . '?' . http_build_query($params), [
+			'headers' => [
+				'Authorization' => 'Bearer ' . $this->api_token,
+				'Accept' => 'application/json'
+			],
+			'timeout' => 30
+		]);
+
+		if (is_wp_error($response)) {
+			return null;
+		}
+
+		return json_decode(wp_remote_retrieve_body($response), true);
+	}
+
+	/**
+	 * Get custom events data
+	 */
+	public function getEvents(string $event_name = '', string $start_date = '', string $end_date = ''): ?array
+	{
+		if (empty($this->api_url) || empty($this->api_token)) {
+			return null;
+		}
+
+		$api_endpoint = rtrim($this->api_url, '/') . "/api/websites/{$this->website_id}/events";
+
+		$params = [];
+		if ($start_date && $end_date) {
+			$params['startAt'] = strtotime($start_date) * 1000;
+			$params['endAt'] = strtotime($end_date) * 1000;
+		}
+		if ($event_name) {
+			$params['event'] = $event_name;
+		}
+
+		$url = $api_endpoint;
+		if (!empty($params)) {
+			$url .= '?' . http_build_query($params);
+		}
+
+		$response = wp_remote_get($url, [
+			'headers' => [
+				'Authorization' => 'Bearer ' . $this->api_token,
+				'Accept' => 'application/json'
+			],
+			'timeout' => 30
+		]);
+
+		if (is_wp_error($response)) {
+			return null;
+		}
+
+		return json_decode(wp_remote_retrieve_body($response), true);
+	}
+
+	/**
+	 * Collect daily analytics data and store in database
+	 */
+	public function collectDailyData(): void
+	{
+		if (!$this->isSetUp() || empty($this->api_token)) {
+			return;
+		}
+
+		$yesterday = date('Y-m-d', strtotime('-1 day'));
+		$data = $this->getAnalytics($yesterday, $yesterday, false);
+
+		if (!$data) {
+			$this->logError('Failed to collect daily data', ['date' => $yesterday]);
+			return;
+		}
+
+		global $wpdb;
+
+		// Store basic metrics
+		$wpdb->insert($this->metrics_table, [
+			'date' => $yesterday,
+			'pageviews' => $data['pageviews']['value'] ?? 0,
+			'visitors' => $data['visitors']['value'] ?? 0,
+			'visits' => $data['visits']['value'] ?? 0,
+			'bounces' => $data['bounces']['value'] ?? 0,
+			'total_time' => $data['totaltime']['value'] ?? 0,
+			'created_at' => current_time('mysql')
+		]);
+
+		// Collect and store events
+		$events = $this->getEvents('', $yesterday, $yesterday);
+		if ($events) {
+			foreach ($events as $event) {
+				$wpdb->insert($this->events_table, [
+					'date' => $yesterday,
+					'event_name' => $event['x'] ?? '',
+					'event_count' => $event['y'] ?? 0,
+					'created_at' => current_time('mysql')
+				]);
+			}
+		}
+
+		$this->logDebug('Successfully collected daily data', ['date' => $yesterday]);
+	}
+
+	/***************************************************************************
+	 * HELPER METHODS
+	 ***************************************************************************/
+
+	/**
+	 * Get current user type for tracking
+	 */
+	private function getCurrentUserType(): string
+	{
+		if (!is_user_logged_in()) {
+			return 'guest';
+		}
+
+		$user = wp_get_current_user();
+		$roles = $user->roles;
+
+		if (empty($roles)) {
+			return 'user';
+		}
+
+		$role = array_values($roles)[0];
+		return str_replace(BASE, '', $role);
+	}
+
+	/**
+	 * Determine traffic source
+	 */
+	private function getTrafficSource(): string
+	{
+		if (isset($_GET['utm_source'])) {
+			return sanitize_text_field($_GET['utm_source']);
+		}
+
+		if (isset($_SERVER['HTTP_REFERER'])) {
+			$referer = $_SERVER['HTTP_REFERER'];
+			$site_url = get_site_url();
+
+			if (strpos($referer, $site_url) === false) {
+				$domain = parse_url($referer, PHP_URL_HOST);
+				return $domain ?: 'external';
+			}
+
+			return 'internal';
+		}
+
+		return 'direct';
+	}
+
+	/**
+	 * Get website ID
+	 */
+	public function getWebsiteId(): string
+	{
+		$this->ensureInitialized();
+		return $this->website_id;
+	}
+
+	/**
+	 * Get tracking script URL
+	 */
+	public function getTrackingScriptUrl(): string
+	{
+		if (!empty($this->api_url)) {
+			return rtrim($this->api_url, '/') . '/script.js';
+		}
+
+		// Default to Umami cloud
+		return 'https://cloud.umami.is/script.js';
+	}
+
+	/***************************************************************************
+	 * REQUIRED ABSTRACT METHOD IMPLEMENTATIONS
+	 ***************************************************************************/
+
+	/**
+	 * Get request headers for API calls
+	 */
+	protected function getRequestHeaders(): array
+	{
+		$headers = [
+			'Accept' => 'application/json',
+			'Content-Type' => 'application/json'
+		];
+
+		if (!empty($this->api_token)) {
+			$headers['Authorization'] = 'Bearer ' . $this->api_token;
+		}
+
+		return $headers;
+	}
+
+	protected function handleRefreshData():array
+	{
+		// Collect today's data
+		$today = date('Y-m-d');
+		$data = $this->getAnalytics($today, $today, false);
+
+		if ($data) {
+			// Clear cache for today
+			$cache_key = md5("analytics_{$today}_{$today}");
+			$this->cache->forget($cache_key);
+
+			return [
+				'success' => true,
+				'message' => 'Data refreshed successfully',
+				'data' => $data
+			];
+		}
+
+		return [
+			'success'	=> false,
+			'message'	=> 'Something went wrong...'
+		];
+	}
+
+	/**
+	 * Process queued operations
+	 */
+	public function processOperation(array|\WP_Error $result, object $operation, array $data): array|\WP_Error
+	{
+		$prefix = strtolower($this->service_name) . '_';
+
+		switch ($operation->type) {
+			case $prefix . 'collect_analytics':
+				$this->collectDailyData();
+				return ['success' => true, 'message' => 'Analytics collected'];
+
+			default:
+				return $result;
+		}
+	}
+
+	/**
+	 * Test API connection
+	 */
+	protected function performConnectionTest(): bool
+	{
+		// Basic tracking only needs website ID
+		if (empty($this->website_id)) {
+			return false;
+		}
+
+		// If API credentials provided, test them
+		if (!empty($this->api_url) && !empty($this->api_token)) {
+			try {
+				$response = wp_remote_get(rtrim($this->api_url, '/') . '/api/websites', [
+					'headers' => [
+						'Authorization' => 'Bearer ' . $this->api_token,
+						'Accept' => 'application/json'
+					],
+					'timeout' => 10
+				]);
+
+				if (is_wp_error($response)) {
+					return false;
+				}
+
+				return wp_remote_retrieve_response_code($response) === 200;
+
+			} catch (Exception $e) {
+				$this->logError('Connection test failed', ['error' => $e->getMessage()]);
+				return false;
+			}
+		}
+
+		// Website ID exists, basic tracking will work
+		return true;
+	}
+
+	/**
+	 * Get service description
+	 */
+	public function getServiceDescription(): string
+	{
+		return "Privacy-focused analytics to understand your website traffic.";
+	}
+
+	/**
+	 * Create database tables for metrics storage
+	 */
+	public static function createTables(): void
+	{
+		global $wpdb;
+		$charset_collate = $wpdb->get_charset_collate();
+
+		// Events table
+		$events_table = $wpdb->prefix . BASE . 'umami_events';
+		$events_sql = "CREATE TABLE IF NOT EXISTS $events_table (
+            id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+            date date NOT NULL,
+            event_name varchar(255) NOT NULL,
+            event_count int(11) NOT NULL DEFAULT 0,
+            event_data longtext,
+            created_at datetime NOT NULL,
+            PRIMARY KEY (id),
+            KEY date_event (date, event_name)
+        ) $charset_collate;";
+
+		// Metrics table
+		$metrics_table = $wpdb->prefix . BASE . 'performance_metrics';
+		$metrics_sql = "CREATE TABLE IF NOT EXISTS $metrics_table (
+            id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+            date date NOT NULL,
+            pageviews int(11) NOT NULL DEFAULT 0,
+            visitors int(11) NOT NULL DEFAULT 0,
+            visits int(11) NOT NULL DEFAULT 0,
+            bounces int(11) NOT NULL DEFAULT 0,
+            total_time int(11) NOT NULL DEFAULT 0,
+            created_at datetime NOT NULL,
+            PRIMARY KEY (id),
+            UNIQUE KEY date_unique (date)
+        ) $charset_collate;";
+
+		require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
+		dbDelta($events_sql);
+		dbDelta($metrics_sql);
+	}
+}
diff --git a/inc/registrar/helpers/AddIntegrationFields.php b/inc/registrar/helpers/AddIntegrationFields.php
index 6aad5fa..952cd51 100644
--- a/inc/registrar/helpers/AddIntegrationFields.php
+++ b/inc/registrar/helpers/AddIntegrationFields.php
@@ -123,7 +123,7 @@
 		}
 
 		if ($this->config->getCanSchedule()) {
-			$fields['schedule_'.$this->service_name] = [
+			$fields['_'.$this->service_name.'_scheduled_at'] = [
 				'type'	=> 'datetime',
 				'label'	=> 'Schedule for later?',
 				'condition' => [

--
Gitblit v1.10.0