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