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/SyncTo.php |  455 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 441 insertions(+), 14 deletions(-)

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

--
Gitblit v1.10.0