Jake Vanderwerf
2026-07-09 c68aefb847b09daa0697de7684d3451e2e68ce1e
=Refactor of Integrations.php to be a bit more useful with a suite of methods for create, update, delete back and forths for integrations where sharing is enabled. Also considering a single instance with a connect method to connect as the site (no user) vs connecting as an individual user - rather than recreating instances for every user.
18 files modified
1105 ■■■■■ changed files
JVBase.php 27 ●●●●● patch | view | raw | blame | history
assets/js/concise/AuthManager.js 6 ●●●● patch | view | raw | blame | history
assets/js/concise/BioManager.js 2 ●●● patch | view | raw | blame | history
assets/js/concise/CRUDOld.js 2 ●●● patch | view | raw | blame | history
assets/js/concise/FormController.js 2 ●●● patch | view | raw | blame | history
base/Site.php 3 ●●●● patch | view | raw | blame | history
inc/helpers/all.php 1 ●●●● patch | view | raw | blame | history
inc/integrations/Facebook.php 35 ●●●●● patch | view | raw | blame | history
inc/integrations/GoogleMyBusiness.php 38 ●●●●● patch | view | raw | blame | history
inc/integrations/Helcim.php 63 ●●●●● patch | view | raw | blame | history
inc/integrations/Instagram.php 23 ●●●● patch | view | raw | blame | history
inc/integrations/Integrations.php 567 ●●●●● patch | view | raw | blame | history
inc/integrations/Square.php 267 ●●●● patch | view | raw | blame | history
inc/managers/queue/Processor.php 22 ●●●● patch | view | raw | blame | history
inc/managers/queue/executors/IntegrationExecutor.php 25 ●●●●● patch | view | raw | blame | history
inc/registrar/Registrar.php 6 ●●●● patch | view | raw | blame | history
inc/registrar/config/Integration.php 9 ●●●●● patch | view | raw | blame | history
inc/registrar/helpers/AddIntegrationFields.php 7 ●●●● patch | view | raw | blame | history
JVBase.php
@@ -86,7 +86,7 @@
    }
    public function __construct()
    private function __construct()
    {
        $this->customBlocks = new CustomBlocks();
        $this->managers = [
@@ -296,17 +296,20 @@
    public function connect(string $service, ?int $userID = null): mixed
    {
        if ($userID) {
            if (!$this->userCanConnect($service, $userID)) {
                return null;
            }
            if (!array_key_exists($service, $this->integrations)) {
                return null;
            }
            return $this->serviceMap[$service]::getInstance($userID);
        }
        return (array_key_exists($service, $this->integrations)) ? $this->integrations[$service] : null;
//      if ($userID) {
//          if (!$this->userCanConnect($service, $userID)) {
//              return null;
//          }
//
//          if (!array_key_exists($service, $this->integrations)) {
//              return null;
//          }
//
//          return $this->serviceMap[$service]::getInstance($userID);
//      }
        return is_null($userID) ?
            $this->integrations[$service]??null :
            (array_key_exists($service, $this->integrations) ? $this->serviceMap[$service]::getInstance($userID) : null);
    }
    public function userCanConnect(string $service, int $userID): bool
assets/js/concise/AuthManager.js
@@ -78,9 +78,9 @@
                'X-WP-Nonce': this.getNonce()
            };
            if (!isFormData && typeof options.body === 'object' && !Object.hasOwn(options.body, 'user')) {
                options.body.user = this.getUser();
            }
            // if (!isFormData && typeof options.body === 'object' && !Object.hasOwn(options.body, 'user')) {
            //  options.body.user = this.getUser();
            // }
            let obj = {
                ...options,
assets/js/concise/BioManager.js
@@ -19,7 +19,7 @@
        if (data === null) {
            return;
        }
        data.user = window.auth.getUser();
        // data.user = window.auth.getUser();
        if (Object.hasOwn(data, 'term_name') && data['term_name'] === ''){
            delete data['term_name'];
assets/js/concise/CRUDOld.js
@@ -508,7 +508,7 @@
        // Get the base content type
        data.content = form.closest('dialog')?.dataset.content || this.content;
        data.user = window.auth.getUser();
        // data.user = window.auth.getUser();
        // Collect timeline entries
        const timelineGroups = form.querySelectorAll('.upload-group');
assets/js/concise/FormController.js
@@ -669,7 +669,7 @@
        this.cancelServerSave(form.id);
        if (form.useQueue && window.jvbQueue) {
            changes.user = window.auth.getUser();
            // changes.user = window.auth.getUser();
            window.jvbQueue.addToQueue({
                endpoint: form.options.endpoint,
                data: changes,
base/Site.php
@@ -130,7 +130,8 @@
    public static function has(string $property):bool
    {
        return property_exists(self::class, $property) && self::${$property} === true;
        $inst = self::getInstance();
        return property_exists($inst, $property) && $inst::${$property} === true;
    }
    public static function hasAny(array $properties):bool
inc/helpers/all.php
@@ -88,4 +88,3 @@
        'post_type'     => $type
    ]);
}
inc/integrations/Facebook.php
@@ -360,31 +360,18 @@
     */
    protected function handleTheSavePost(int $postID, WP_Post $post, bool $update, array $settings): void
    {
        $post_type = $settings['fb_type'] ?? 'post';
        $sync_immediately = $settings['immediate'] ?? false;
        // Determine the Facebook post type
        $fb_endpoint = self::FB_POST_TYPES[$post_type] ?? 'feed';
        $operation_data = [
            'post_id' => $postID,
            'fb_type' => $post_type,
            'endpoint' => $fb_endpoint,
            'page_id' => $this->page_id
        ];
        // Check for scheduled posting
        $schedule_time = get_post_meta($postID, BASE . 'schedule_facebook', true);
        $options = [];
        if ($schedule_time && strtotime($schedule_time) > time()) {
            $options['scheduled'] = strtotime($schedule_time);
        } elseif (!$sync_immediately) {
            $options['delay'] = 300; // 5 minute delay for batching
        if (!$this->hasOAuthCredentials()) {
            error_log('OAuth Not set up for '.$this->service_name);
            return;
        }
        $operation = $update ? 'update_post' : 'create_post';
        $this->queueOperation($operation, $operation_data, $options);
        $this->queueOperation(self::$syncTo, [
            'items'     => [$postID],
            'user'      => user_can($post->post_author, 'manage_options') ? null : $post->post_author
        ], [
            'delay' => 60
        ]);
        Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued');
    }
    /**
@@ -393,7 +380,7 @@
    public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array
    {
        try {
            $fb = (array_key_exists('user', $data)) ? new self((int)$data['user']) : $this;
            $fb = (array_key_exists('user', $data) && $data['user'] !== 0) ? $this->getInstance((int)$data['user']) : $this;
            switch ($operation->type) {
                case 'facebook_create_post':
                    return $fb->createFacebookPost($data);
inc/integrations/GoogleMyBusiness.php
@@ -216,39 +216,25 @@
    public function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings):void
    {
        $type = $settings['content_type']??'post'; //can be 'post', 'offer', 'event', 'hours', 'info',
        $initial = $settings['initial']?? false;
        $syncOnUpdate = $settings['update']??false;
        if ($update && !$syncOnUpdate) {
            return;
        }
        if (!$initial) {
        if (!$this->hasOAuthCredentials()) {
            error_log('OAuth Not set up for '.$this->service_name);
            return;
        }
        $options = $data = [];
        switch ($type) {
            case 'menu_item':
                $options = ['delay' => 60];
                break;
        }
        if (!user_can($post->post_author, 'manage_options')) {
            $data['user'] = $post->post_author;
        }
        $data['post_id'] = $postID;
        $operation = ($update) ? 'update_' : 'create_';
        $this->queueOperation(
            $operation.$type,
            $data,
            $options
        );
        $this->queueOperation(self::$syncTo, [
            'items'     => [$postID],
            'user'      => user_can($post->post_author, 'manage_options') ? null : $post->post_author
        ], [
            'delay' => 60
        ]);
        Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued');
    }
    public function processOperation(\WP_Error|array $result, object $operation, array $data):\WP_Error|array
    {
        try {
            $gmb = (array_key_exists('user', $data)) ? new self((int)$data['user']) : $this;
            $gmb = (array_key_exists('user', $data) && $data['user'] !== 0) ? $this->getInstance((int)$data['user']) : $this;
            $gmb->ensureInitialized();
            switch ($operation->type) {
                case 'gmb_create_menu_item':
@@ -481,7 +467,7 @@
                ];
            }
            $gmb = new self($data['userID']??null);
            $gmb = self::getInstance($data['userID']??null);
            // Build the complete menu structure
            $menu_data = $gmb->collectMenu($menu_items);
inc/integrations/Helcim.php
@@ -2,6 +2,7 @@
namespace JVBase\integrations;
use Exception;
use JVBase\meta\Meta;
use WP_Error;
use WP_Post;
use JVBase\ui\Checkout;
@@ -25,6 +26,7 @@
 */
class Helcim extends Integrations
{
    protected static string $syncCustomer = 'helcim_sync_customer';
    protected string $service_name = 'helcim';
    protected string|array $apiBase = 'https://api.helcim.com/v2';
@@ -711,9 +713,6 @@
            return $desc;
        }, 10, 2);
        // Register queue operation types with IntegrationExecutor
        $this->registerQueueTypes();
        // Register webhook endpoint (handled by parent)
        $this->registerWebhookEndpoint();
    }
@@ -760,38 +759,16 @@
    }
    protected function registerQueueTypes(): void
    protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void
    {
        $queue    = JVB()->queue();
        $executor = new IntegrationExecutor();
        $queue->registry()->register('helcim_sync_to', new TypeConfig(
            executor:  $executor,
            chunkKey:  'items',
            chunkSize: 10,
            maxRetries: 3
        ));
        $queue->registry()->register('helcim_sync_from', new TypeConfig(
            executor:  $executor,
            chunkKey:  'items',
            chunkSize: 10,
            maxRetries: 3
        ));
        $queue->registry()->register('helcim_delete_from', new TypeConfig(
            executor:  $executor,
            chunkKey:  'external_ids',
            chunkSize: 20,
            maxRetries: 2
        ));
        $queue->registry()->register('helcim_import', new TypeConfig(
        $queue->registry()->register(self::$import, new TypeConfig(
            executor:   $executor,
            maxRetries: 3
        ));
        $queue->registry()->register('helcim_sync_customer', new TypeConfig(
        $queue->registry()->register(self::$syncCustomer, new TypeConfig(
            executor:   $executor,
            maxRetries: 2
        ));
@@ -1028,22 +1005,34 @@
    protected function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void
    {
        $fields = $this->getSyncFields($postID, 'post', ['share_to_helcim', 'schedule_helcim']);
        if (empty($fields['share_to_helcim'])) {
            return;
        }
        // Uses IntegrationExecutor via TypeRegistry instead of FilteredExecutor
        $this->queueOperation('sync_to', [
        error_log('==== [Helcim]::handleTheSavePost ====');
        $this->queueOperation(self::$syncTo, [
            'items'   => [$postID],
            'user_id' => $this->userID,
            'user'      => user_can($post->post_author, 'manage_options') ? null : $post->post_author
        ], [
            'priority' => 'high',
            'delay'    => 30,
        ]);
        Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued');
    }
    /**
     * Handle post deletion
     */
    public function handleDeletePost(int $postID): void
    {
        $item_id = Meta::forPost($postID)->get("_{$this->service_name}_item_id");
        update_post_meta($postID, BASE . '_helcim_sync_status', 'queued');
        if (empty($item_id)) {
            return;
        }
        $this->queueOperation(self::$deleteFrom, [
            'external_ids' => [$item_id],
            'post_id'      => $postID,
        ], [
            'priority' => 'high',
        ]);
    }
    protected function handleImportFromHelcim(): array
inc/integrations/Instagram.php
@@ -6,6 +6,7 @@
namespace JVBase\integrations;
use JVBase\integrations\Integrations;
use JVBase\meta\Meta;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
@@ -261,24 +262,22 @@
     */
    public function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void
    {
        // Check if post has featured image (Instagram requirement)
        if (!$this->hasOAuthCredentials()) {
            error_log('OAuth Not set up for '.$this->service_name);
            return;
        }
        if (!has_post_thumbnail($postID)) {
            $this->logError('Cannot post to Instagram without featured image', [
                'post_id' => $postID
            ]);
            return;
        }
        // Queue the Instagram post creation
        $this->queueOperation('create_post', [
            'post_id' => $postID,
            'type' => get_post_type($postID)
        $this->queueOperation(self::$syncTo, [
            'items'     => [$postID],
            'user'      => user_can($post->post_author, 'manage_options') ? null : $post->post_author
        ], [
            'priority' => 'normal',
            'delay' => 60 // Wait 1 minute to ensure all metadata is saved
            'delay' => 60
        ]);
        update_post_meta($postID, BASE . '_instagram_sync_status', 'queued');
        Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued');
    }
    /**
inc/integrations/Integrations.php
@@ -3,6 +3,9 @@
use Exception;
use JVBase\managers\Cache;
use JVBase\managers\queue\executors\IntegrationExecutor;
use JVBase\managers\queue\mergers\DefaultMerger;
use JVBase\managers\queue\TypeConfig;
use JVBase\meta\Form;
use JVBase\meta\Meta;
use JVBase\managers\ErrorHandler;
@@ -10,8 +13,11 @@
use JVBase\registrar\Registrar;
use WP_Error;
use WP_Post;
use WP_Query;
use WP_REST_Request;
use WP_REST_Response;
use WP_Term;
use WP_User;
if (!defined('ABSPATH')) {
    exit;
@@ -30,15 +36,19 @@
{
    //Flag to allow for custom settings (defaults, etc) for an integration in the dashboard
    public static bool $hasExtraOptions = false;
    public bool $hasBatchCreate = false;
    public bool $hasBatchUpdate = false;
    public bool $hasBatchDelete = false;
    public bool $canCreateOnUpdate = false;
    /**
     * Queue types
     * These types match with IntegrationExecutor
     */
    protected static string $syncTo = 'sync_to';
    protected static string $deleteFrom = 'delete_from';
    protected static string $syncFrom = 'sync_from';
    protected static string $syncCustomer = 'sync_customer';
    protected static string $import = 'import';
    protected static string $syncTo;
    protected static string $deleteFrom;
    protected static string $syncFrom;
    protected static string $syncCustomer;
    protected static string $import;
    /**
     * API Configuration
     * These properties define how the integration connects to external services
@@ -208,9 +218,13 @@
            ];
        }
        self::$syncTo = $this->service_name.'_sync_to';
        self::$deleteFrom = $this->service_name.'_delete_from';
        self::$syncFrom = $this->service_name.'_sync_from';
        //TODO: What is the difference between sync_from and import?
        self::$import = $this->service_name.'import_from';
        add_filter('jvbShouldRenderMeta', [$this, 'checkRenderField'], 10, 4);
    }
    protected function addFilters():bool
@@ -312,21 +326,7 @@
    protected function getPostTypes(): void
    {
        $key = BASE . $this->service_name . '_sync_post_types';
        $postTypes = get_option($key, false);
        if (!$postTypes) {
            $postTypes = [];
            foreach (Registrar::getRegistered('post') as $registrar) {
                $registrar = Registrar::getInstance($registrar);
                if ($registrar->hasIntegration($this->service_name)) {
                    $postTypes[] = $registrar->getSlug();
                }
            }
            update_option($key, $postTypes);
        }
        $this->syncPostTypes = $postTypes;
        $this->syncPostTypes = Registrar::withIntegration($this->service_name);
    }
    protected function getTaxonomies():void
@@ -687,9 +687,9 @@
    protected function clearCache():array
    {
        $success = $this->cache->flush();
        $this->cache->flush();
        return [
            'success'   => $success,
            'success'   => true,
        ];
    }
@@ -1058,11 +1058,8 @@
    ):bool {
        $queue = JVB()->queue();
        // Add service prefix to operation type
        $operation_type = strtolower($this->service_name) . '_' . $type;
        $queued =  $queue->queueOperation(
            $operation_type,
            $type,
            $this->userID ?? 0,
            array_merge($data, ['service' => $this->service_name, 'user' => $this->userID??0]),
            array_merge([
@@ -1204,31 +1201,85 @@
     */
    protected function registerHooks(): void
    {
        add_filter(BASE . 'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
        add_action( 'wp_ajax_'.BASE.$this->service_name.'_oauth_callback', [$this, 'handleAjaxResponse'] );
        add_action( 'wp_ajax_nopriv_'.BASE.$this->service_name.'_oauth_callback', [$this, 'handleAjaxResponse'] );
        if ($this->handleWebhooks){
        //Handled by IntegrationExecutor now
//      add_filter(BASE . 'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
        if (is_null($this->userID)) {
            add_action( 'wp_ajax_'.BASE.$this->service_name.'_oauth_callback', [$this, 'handleAjaxResponse'] );
            add_action( 'wp_ajax_nopriv_'.BASE.$this->service_name.'_oauth_callback', [$this, 'handleAjaxResponse'] );
        }
        if ($this->handleWebhooks && is_null($this->userID)){
            $this->registerWebhookEndpoint();
        }
        // Let child classes register additional hooks if needed
        $this->registerAdditionalHooks();
        if (!empty($this->syncPostTypes)) {
            add_action('save_post', [$this, 'handleSavePost'], 20, 3);
        if (!empty($this->syncPostTypes) && is_null($this->userID)) {
            $this->addSavePost();
            add_action('transition_post_status', [$this, 'handlePostStatusTransition'], 10, 3);
            if ($this->canSync['delete']) {
                add_action('before_delete_post', [$this, 'handleDeletePost'], 10, 1);
            }
        }
        if (!empty($this->syncTaxonomies)) {
        if (!empty($this->syncTaxonomies) && is_null($this->userID)) {
            add_action('saved_term', [$this, 'handleSaveTerm'], 20, 5);
            if ($this->canSync['delete']) {
                add_action('pre_delete_term', [$this, 'handleDeleteTerm'], 10, 2);
            }
        }
    }
        add_action('init', [$this, 'registerQueueTypes'], 10);
    }
        public function addSavePost():void
        {
            if (!has_action('save_post', [$this, 'handleSavePost'])) {
                add_action('save_post', [$this, 'handleSavePost'], 20, 3);
            }
        }
        public function removeSavePost():void
        {
            remove_action('save_post', [$this, 'handleSavePost'], 20, 3);
        }
        public function registerQueueTypes():void
        {
            if (empty($this->syncTaxonomies) && empty($this->syncPostTypes)) {
                return;
            }
            $queue = JVB()->queue();
            $executor = new IntegrationExecutor();
            $queue->registry()->register(self::$syncTo, new TypeConfig(
                mergeable: new DefaultMerger('items'),
                executor: $executor,
                chunkKey: 'items',
                chunkSize: 50,
                maxRetries: 3,
            ));
            if ($this->canSync['delete']) {
                $queue->registry()->register(self::$deleteFrom, new TypeConfig(
                    mergeable: new DefaultMerger('external_ids'),
                    executor: $executor,
                    chunkKey: 'external_ids',
                    chunkSize: 200,
                    maxRetries: 2
                ));
            }
            $queue->registry()->register(self::$syncFrom, new TypeConfig(
                executor: $executor,
                maxRetries: 3
            ));
            $this->registerAdditionalQueueTypes($executor);
        }
            protected function registerAdditionalQueueTypes(IntegrationExecutor $executor):void
            {
                //Empty. Integration extensions can register additional operation types from here.
            }
    public function handleAjaxResponse()
    {
@@ -2097,6 +2148,11 @@
     */
    public function handleSavePost(int $postID, WP_Post $post, bool $update): void
    {
        if (!is_null($this->userID)) {
            return;
        }
        error_log('=== ['.$this->service_name.']::handleSavePost called');
        if (!$postID || $postID === 0) {
            return;
        }
@@ -2105,40 +2161,50 @@
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
        if (wp_is_post_revision($postID)) return;
        if (empty($this->syncPostTypes) || !in_array(jvbNoBase($postType), $this->syncPostTypes)) {
            error_log('Not handling save for '.$this->service_name.' because there are no syncPostTypes: '.print_r($this->syncPostTypes, true));
            return;
        }
        $registrar = Registrar::getInstance($postType);
        if (!$registrar){
            return;
        }
        if (empty($this->syncPostTypes)) {
            return;
        }
        $settings = $registrar->hasIntegration($this->service_name)??null;
        if (!$settings) {
            error_log('Not handling save for '.$this->service_name.' because of no registrar settings');
            return;
        }
        $settings = $registrar->getIntegrationConfig($this->service_name);
        if (!$settings){
            error_log('Not handling save for '.$this->service_name.' because of no integration config '.print_r($settings, true));
            return;
        }
        $fields = $this->getSyncFields($postID, 'post', ['schedule_'.$this->service_name]);
        if (!$fields['share_to_'.$this->service_name]) {
            error_log('Not handling save for '.$this->service_name.' because of no share_to_'.$this->service_name.' '.print_r($fields, true));
            return;
        }
        $isShared = isset($fields["_{$this->service_name}_item_id"]);
        if ($update && $isShared && !$fields['_keep_synced_'.$this->service_name]) {
            error_log('Not handling save for '.$this->service_name.' because it is already shared, and not set to keep synced. ');
            return;
        }
        if ($post->post_status !== 'publish' && !$isShared) {
            error_log('Not handling save for '.$this->service_name.' because post status is not publish, and it is not already shared.');
            return;
        }
        error_log('==== Sending to integration\'s handleTheSavePost '.$this->service_name.' ====');
        $this->removeSavePost();
        $this->handleTheSavePost($postID, $post, $update, $settings);
        $this->addSavePost();
    }
@@ -3518,4 +3584,425 @@
    {
        return $this->icon;
    }
    public function getServiceItemID(int $itemID, string $type = 'post'):string
    {
        return match($type) {
            'post'  => Meta::forPost($itemID)->get("_{$this->service_name}_item_id"),
            'term','taxonomy'   => Meta::forTerm($itemID)->get("_{$this->service_name}_item_id"),
            'user'  => Meta::forUser($itemID)->get("_{$this->service_name}_item_id"),
            default => ''
        };
    }
    public function canBatchUpdate():bool
    {
        return $this->hasBatchUpdate;
    }
    public function canBatchCreate():bool
    {
        return $this->hasBatchCreate;
    }
    public function canBatchDelete():bool
    {
        return $this->hasBatchDelete;
    }
    protected function updateItemStatus(int|array $items, string $status = 'pending', string $type = 'post'):void
    {
        if (!is_array($items)) {
            $items = [$items];
        }
        foreach ($items as $ID) {
            match ($type) {
                'post'  => Meta::forPost($ID)->set('_'.$this->service_name.'_sync_status', $status),
                'term'  => Meta::forTerm($ID)->set('_'.$this->service_name.'_sync_status', $status),
                'user'  => Meta::forUser($ID)->set('_'.$this->service_name.'_sync_status', $status),
                default => false
            };
        }
    }
    public function updateBatchToService(array $data):array
    {
        $type = $data['type']??'post';
        $newlyCreated = [];
        if (!$this->canCreateOnUpdate) {
            $created = array_filter($data['items'], function($ID) use ($type) {
                return !empty($this->getServiceItemID($ID, $type));
            });
            //Test to see if we have any that haven't been created yet.
            //For some services, items may have to be created before they can be updated
            if (count($created) !== count($data['items'])) {
                $notCreated = array_filter($data['items'], function($ID) use ($type) {
                    return empty($this->getServiceItemID($ID, $type));
                });
                $newData = $data;
                $newData['items'] = $notCreated;
                $newlyCreated = $this->createBatchToService($newData);
            }
            // If we don't have any that are created, just send the noUpdatedItems response, with any newly created items added
            if (empty($created)) {
                return $this->noUpdatedItems($newlyCreated);
            }
            $data['items'] = $created;
        }
        $items = $this->formatItems($data['items'], $type);
        if (empty($items)) {
            return $this->noUpdatedItems($newlyCreated);
        }
        if ($this->hasBatchUpdate) {
            $response = $this->sendBatchUpdate($items);
            if (!is_wp_error($response)) {
                $result = $this->processBatchUpdateResponse($data, $response);
            } else {
                $this->logError('Batch update failed',[
                    'method'    => 'updateBatchToService',
                    'item_ids'  => $data['items'],
                    'error'     => $response
                ]);
                $this->updateItemStatus($data['items'], 'error');
                $result = [
                    'outcome'   => 'failed_permanent',
                    'result'    => 'Could not update items'
                ];
            }
        } else {
            $success = $errors = [];
            //Does not have batch update, manually update each one
            foreach ($items as $item) {
                $itemResult = $this->updateOne($item);
                if (!is_wp_error($itemResult)) {
                    $success[] = $itemResult;
                } else {
                    $errors[] = $itemResult;
                }
            }
            $result = [
                'outcome'   => empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial'),
                'result'    => [
                    'success'   => $success,
                    'errors'    => $errors
                ]
            ];
        }
        return array_merge($result, $newlyCreated);
    }
    /**
     * To be implemented by integration extension. Updates a single item
     * @param array $item
     * @return array|WP_Error
     */
        protected function updateOne(array $item):array|WP_Error
        {
            return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own updateOne method');
        }
    /**
     * Overridden by child classes
     * @param array $items
     * @return array|WP_Error
     */
        protected function sendBatchUpdate(array $items):array|WP_Error
        {
            return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own sendBatchUpdate');
        }
    /**
     * Overridden by child classes
     * @param array $items
     * @return array|WP_Error
     */
        protected function sendBatchCreate(array $items):array|WP_Error
        {
            return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own sendBatchCreate');
        }
    /**
     * To be implemented by extensions
     * @param array $data
     * @param array $response
     * @return array
     */
        protected function processBatchUpdateResponse(array $data, array $response):array
        {
            return [
                'outcome'   => 'failed',
                'result'    => [
                    'message'   => $this->service_name.' should implement processBatchUpdateResponse.'
                ]
            ];
        }
    /**
     * To be implemented by extensions
     * @param array $data
     * @param array $response
     * @return array
     */
        protected function processBatchCreateResponse(array $data, array $response):array
        {
            return [
                'outcome'   => 'failed',
                'result'    => [
                    'message'   => $this->service_name.' should implement processBatchCreateResponse.'
                ]
            ];
        }
    /**
     * Defaults to an array of formatted items. Can be overridden in child classes
     * @param array $itemIDs
     * @param string $type
     * @return array
     */
        protected function formatItems(array $itemIDs, string $type):array
        {
            $items = [];
            foreach ($itemIDs as $ID) {
                try {
                    $items[] = $this->formatForService($ID, $type);
                } catch (Exception $e){
                    $this->logError('Could not format Item for service',[
                        'itemID' => $ID,
                        'type'  => $type,
                        'message'   => $e->getMessage(),
                    ]);
                }
            }
            return $items;
        }
    /**
     * Must be defined according to how each service needs it to be
     * @param int $itemID
     * @param string $type
     * @return array
     * @throws Exception
     */
        protected function formatForService(int $itemID, string $type = 'post'):array
        {
            throw new Exception('formatForService must be implemented by child class');
        }
        protected function noUpdatedItems(array $created):array
        {
            $result =  [
                'outcome'   => 'success',
                'result'    => [
                    'message'   => 'No items to update',
                    'updated'   => [],
                    'errors'    => [],
                ]
            ];
            if (!empty($created)) {
                error_log('Result before: '.print_r($result, true));
                $result = array_merge($result, $created);
                error_log('Result after merge: '.print_r($result, true));
            }
            return $result;
        }
        protected function noCreatedItems(array $updated):array
        {
            $result = [
                'outcome'   => 'success',
                'result'    => [
                    'message'   => 'No items to create',
                    'created'   => [],
                    'errors'    => [],
                ]
            ];
            if (!empty($updated)) {
                $result = array_merge($result, $updated);
            }
            return $result;
        }
        public function createBatchToService($data):array
        {
            $type = $data['type']??'post';
            //Check if any of the submitted ids are already created
            $created = array_filter($data['items'],
                function($ID) use ($type) {
                    return !empty($this->getServiceItemID($ID, $type));
                });
            $updated = [];
            if (!empty($created)) {
                $updateData = $data;
                $updateData['items'] = $created;
                $updated = $this->updateBatchToService($data);
                //remove any updated items from the original items to process
                $data['items'] = array_filter($data['items'], function ($ID) use ($created) {
                    return !in_array($ID, $created);
                });
            }
            $items = $this->formatItems($data['items'], $type);
            if (empty($items)) {
                return $this->noCreatedItems($updated);
            }
            $response = [
                'outcome'   => 'failed',
                'result'    => [
                    'message'   => 'No result'
                ]
            ];
            if ($this->hasBatchCreate) {
                $response = $this->sendBatchCreate($items);
                if (!is_wp_error($response)) {
                    $result = $this->processBatchCreateResponse($data, $response);
                } else {
                    $this->logError('Batch create failed',[
                        'method'    => 'createBatchToService',
                        'item_ids'  => $data['items'],
                        'error'     => $response
                    ]);
                    $this->updateItemStatus($data['items'], 'error');
                    $result = [
                        'outcome'   => 'failed_permanent',
                        'result'    => 'Could not update items'
                    ];
                }
            } else {
                $success = $errors = [];
                foreach ($items as $item) {
                    $itemResult = $this->createOne($item);
                    if (!is_wp_error($itemResult)) {
                        $success[] = $itemResult;
                    } else {
                        $errors[] = $itemResult;
                    }
                }
                $result = [
                    'outcome'   => empty($errors) ? 'success' : (empty($success) ? 'failed' : 'partial'),
                    'result'    => [
                        'success'   => $success,
                        'errors'    => $errors
                    ]
                ];
            }
            return array_merge($result, $updated);
        }
        /**
         * To be implemented by integration extension. Updates a single item
         * @param array $item
         * @return array|WP_Error
         */
        protected function createOne(array $item):array|WP_Error
        {
            return new WP_Error('invalid', 'Integration '.$this->service_name.' must implement its own createOne');
        }
    public function findWPItem(string $integrationID, ?string $type = null):array|false
    {
        if (empty($integrationID)) {
            error_log('Integrations::findWPItem No Integration ID received.');
            return false;
        }
        $types = ['post', 'term', 'user'];
        if (!is_null($type) && !in_array($type, $types)) {
            error_log('Integrations::findWPItem invalid type sent: '.$type.'; defaulting to checking all');
            $type = null;
        }
        if (!is_null($type)) {
            return $this->queryWPType($integrationID, $type);
        } else {
            foreach ($types as $type) {
                $check = $this->queryWPType($integrationID, $type);
                if ($check) {
                    return $check;
                }
            }
        }
        return false;
    }
    /**
     * @param string $integrationID
     * @param string|null $type
     * @return array|false if success, an array of $ID and $type
     */
        public function queryWPType(string $integrationID, ?string $type = null):array|false
        {
            if (!in_array($type, ['post', 'term', 'user'])) {
                return $this->findWPItem($integrationID);
            }
            return match($type) {
                'post'  => $this->findWPPost($integrationID),
                'term'  => $this->findWPTerm($integrationID),
                'user'  => $this->findWPUser($integrationID),
                default => false
            };
        }
        public function findWPPost(string $integrationID):array|false
        {
            if (empty($integrationID)) {
                return false;
            }
            $get = new WP_Query([
                'meta_query'    => [
                    'key'   => BASE."_{$this->service_name}_item_id",
                    'value' => $integrationID,
                ],
                'fields'    => 'ids',
                'posts_per_page' => 1,
            ]);
            return $get->have_posts() && !empty($get->posts) ? [$get->posts[0], 'post'] : false;
        }
        public function findWPTerm(string $integrationID):array|false
        {
            if (empty($integrationID)) {
                return false;
            }
            $get = get_terms([
                'meta_query'    => [
                    'key'   => BASE."_{$this->service_name}_item_id",
                    'value' => $integrationID,
                ],
                'fields'    => 'ids',
                'hide_empty'=> true,
                'number'    => 1
            ]);
            return is_wp_error($get) || empty($get) ? false : [$get[0], 'term'];
        }
        public function findWPUser(string $integrationID):array|false
        {
            if (empty($integrationID)) {
                return false;
            }
            $get = get_users([
                'meta_query'    => [
                    'key'   => BASE."_{$this->service_name}_item_id",
                    'value' => $integrationID,
                ],
                'fields'    => 'ids',
                'number'    => 1,
            ]);
            return is_wp_error($get) || empty($get) ? false : [$get[0], 'user'];
        }
    /*********************************************************************************
     * Syncing WP items from Service
    *********************************************************************************/
    protected function createItemFromService(array $itemData):bool
    {
    }
}
inc/integrations/Square.php
@@ -28,6 +28,7 @@
 */
class Square extends Integrations
{
    protected static string $syncCustomer = 'square_sync_customer';
    protected array $allowedContent = [
        'REGULAR',
        'FOOD_AND_BEV',
@@ -90,14 +91,16 @@
    public static function getInstance(?int $userID = null):self
    {
        $key = is_null($userID) ? 'base' : $userID;
        if (!array_key_exists($key, self::$instances)) {
            self::$instances[$key] = new static($userID);
            self::$instances[$key] = new self($userID);
        }
        return self::$instances[$key];
    }
    protected function __construct(?int $userID = null)
    {
        // Display properties
        $this->title = 'Square';
        $this->icon = 'square-logo';
@@ -786,8 +789,8 @@
                        'name' => $variation['name'],
                        'pricing_type' => 'FIXED_PRICING',
                        'price_money' => [
                            'amount' => intval($variation['_square_price'] * 100), // Convert to cents
                            'currency' => 'USD'
                            'amount' => $this->formatPrice($variation['_square_price']??0),
                            'currency' => $this->getCurrency()
                        ]
                    ]
                ];
@@ -801,8 +804,8 @@
                    'name' => 'Regular',
                    'pricing_type' => 'FIXED_PRICING',
                    'price_money' => [
                        'amount' => intval(($itemData['_square_price'] ?? 0) * 100),
                        'currency' => 'USD'
                        'amount' => $this->formatPrice($itemData['_square_price'] ?? 0),
                        'currency' => $this->getCurrency()
                    ]
                ]
            ];
@@ -858,7 +861,6 @@
            return;
        }
        add_action('wp_login', [$this, 'trackUserLogin'], 10, 2);
        add_action('wp_enqueue_scripts', [$this, 'enqueueScripts']);
        add_filter('jvbAdditionalActions', [Checkout::class, 'render']);
@@ -880,8 +882,6 @@
        add_filter('jvb_checkout_browse_text', function () {
            return 'browse our menu';
        });
        // Register queue executor types
        add_action('init', [$this, 'registerQueueTypes'], 10);
    }
    /**
@@ -910,29 +910,9 @@
            ]);
    }
    public function registerQueueTypes(): void
    protected function registerAdditionalQueueTypes(IntegrationExecutor $executor): void
    {
        $queue    = JVB()->queue();
        $executor = new IntegrationExecutor();
        $queue->registry()->register(self::$syncTo, new TypeConfig(
            executor:   $executor,
            chunkKey:   'items',
            chunkSize:  50,
            maxRetries: 3
        ));
        $queue->registry()->register(self::$deleteFrom, new TypeConfig(
            executor:   $executor,
            chunkKey:   'external_ids',
            chunkSize:  200,
            maxRetries: 2
        ));
        $queue->registry()->register(self::$syncFrom, new TypeConfig(
            executor:   $executor,
            maxRetries: 3
        ));
        $queue->registry()->register(self::$syncCustomer, new TypeConfig(
            executor:   $executor,
@@ -954,15 +934,15 @@
     */
    protected function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void
    {
        error_log('==== [Square]::handleTheSavePost ====');
        $this->queueOperation(self::$syncTo, [
            'items'   => [$postID],
            'user_id' => $this->userID,
            'user'      => user_can($post->post_author, 'manage_options') ? null : $post->post_author
        ], [
            'priority' => 'high',
            'delay'    => 30,
        ]);
        update_post_meta($postID, BASE . '_square_sync_status', 'queued');
        Meta::forPost($postID)->set('_'.$this->service_name.'_sync_status', 'queued');
    }
    /**
@@ -970,16 +950,18 @@
     */
    public function handleDeletePost(int $postID): void
    {
        $square_id = get_post_meta($postID, BASE . '_square_catalog_id', true);
        $item_id = $this->getServiceItemID($postID);
        if ($square_id) {
            $this->queueOperation(self::$deleteFrom, [
                'external_ids' => [$square_id],
                'post_id'      => $postID,
            ], [
                'priority' => 'high',
            ]);
        if (empty($item_id)) {
            return;
        }
        $this->queueOperation(self::$deleteFrom, [
            'external_ids' => [$item_id],
            'post_id'      => $postID,
        ], [
            'priority' => 'high',
        ]);
    }
    /**
@@ -1063,6 +1045,9 @@
    private function processBatchSyncResponse(array $response, array $map, array &$success, array &$errors):void
    {
        error_log('==== SQUARE::processBatchSyncResponse =====');
        error_log('Full response: '.print_r($response, true));
        // Handle successful objects
        if (!empty($response['objects'])) {
            foreach ($response['objects'] as $object) {
@@ -1162,6 +1147,14 @@
        // Get existing Square catalog ID if it exists
        $existing_square_id = get_post_meta($postID, BASE . '_square_catalog_id', true);
        $registrar = Registrar::getInstance($post_type);
        $product_type = 'FOOD_AND_BEV';
        if ($registrar) {
            $conf = $registrar->getIntegration($this->service_name);
            if ($conf) {
                $product_type = $conf->getContentType();
            }
        }
        // Build the base catalog object
        $catalog_object = [
@@ -1170,7 +1163,7 @@
            'item_data' => [
                'name' => $post->post_title,
                'description' => wp_strip_all_tags($post->post_content),
                'product_type' => 'FOOD_AND_BEV',
                'product_type' => $product_type,
                'variations' => [],
                'is_taxable' => true,
            ]
@@ -1182,10 +1175,9 @@
        }
        // Add variations
        $variations = $meta->get('product_variations');
        $variations = $meta->get('_square_product_variations');
        if (empty($variations)) {
            // Create default variation if none exist
            $price = floatval($meta->get('price') ?: 0);
            $catalog_object['item_data']['variations'][] = [
                'type' => 'ITEM_VARIATION',
                'id' => $existing_square_id ? null : '#'.BASE.'menu_item_' . $postID . '_var_default',
@@ -1194,32 +1186,41 @@
                    'ordinal' => 0,
                    'pricing_type' => 'FIXED_PRICING',
                    'price_money' => [
                        'amount' => intval($price * 100), // Convert dollars to cents
                        'currency' => 'CAD'
                        'amount' => $this->formatPrice($meta->get('_square_price')),
                        'currency' => $this->getCurrency()
                    ],
                    'sellable' => true,
                    'stockable' => true
                ]
            ];
        } else {
            $resetVariations = false;
            foreach ($variations as $index => $variation) {
                $existing_var_id = get_post_meta($postID, BASE . '_square_variation_' . $index . '_id', true);
                $id = '#'.BASE.'menu_item_' . $postID . '_var_' . $index;
                if (empty($variation['item_id'])) {
                    $resetVariations = true;
                    $variations[$index]['item_id'] = $id;
                    $variation['item_id'] = $id;
                }
                $catalog_object['item_data']['variations'][] = [
                    'type' => 'ITEM_VARIATION',
                    'id' => $existing_var_id ?: '#'.BASE.'menu_item_' . $postID . '_var_' . $index,
                    'id' => $variation['item_id'],
                    'item_variation_data' => [
                        'name' => $variation['name'] ?? 'Variation ' . ($index + 1),
                        'ordinal' => $index,
                        'pricing_type' => 'FIXED_PRICING',
                        'price_money' => [
                            'amount' => intval(floatval($variation['price'] ?? 0) * 100),
                            'currency' => 'CAD'
                            'amount' =>$this->formatPrice($variation['price'] ?? 0),
                            'currency' => $this->getCurrency()
                        ],
                        'sellable' => true,
                        'stockable' => true
                    ]
                ];
            }
            if ($resetVariations) {
                $meta->set('_square_product_variations', $variations);
            }
        }
        // Add categories if they exist
@@ -1262,98 +1263,7 @@
        return $catalog_object;
    }
    /**
     * Build variations from repeater field
     */
    private function buildVariations(int $postID, array $values, string $post_type): array
    {
        $variations = [];
        $product_variations = $values['product_variations'] ?? [];
        // Get variation field mapping
        $variation_map = $this->getVariationMapping($post_type);
        // If we have repeater variations
        if (!empty($product_variations) && is_array($product_variations)) {
            foreach ($product_variations as $index => $variation_data) {
                // Skip empty variations
                if (empty($variation_data['name']) && empty($variation_data['_square_sku'])) {
                    continue;
                }
                $variation = [
                    'type' => 'ITEM_VARIATION',
                    'id' => '#' . $post_type . '_' . $postID . '_var_' . $index,
                    'item_variation_data' => [
                        'name' => $variation_data['name'] ?? 'Variation ' . ($index + 1),
                        'ordinal' => $index,
                        'pricing_type' => 'FIXED_PRICING'
                    ]
                ];
                // Check for existing Square variation ID
                $square_var_id = get_post_meta($postID, BASE . '_square_variation_' . $index . '_id', true);
                if ($square_var_id) {
                    $variation['id'] = $square_var_id;
                }
                // Map variation fields
                foreach ($variation_map as $square_field => $wp_field) {
                    if (isset($variation_data[$wp_field])) {
                        switch ($square_field) {
                            case 'price':
                                $variation['item_variation_data']['price_money'] = [
                                    'amount' => intval($variation_data[$wp_field] * 100), // Convert to cents
                                    'currency' => 'CAD'
                                ];
                                break;
                            case 'sku':
                                $variation['item_variation_data']['_square_sku'] = $variation_data[$wp_field];
                                break;
                            case 'track_inventory':
                                $variation['item_variation_data']['track_inventory'] = (bool)$variation_data[$wp_field];
                                break;
                            case 'service_duration':
                                if (!empty($variation_data[$wp_field])) {
                                    $variation['item_variation_data']['service_data'] = [
                                        'duration_minutes' => intval($variation_data[$wp_field])
                                    ];
                                }
                                break;
                            default:
                                $variation['item_variation_data'][$square_field] = $variation_data[$wp_field];
                                break;
                        }
                    }
                }
                $variations[] = $variation;
            }
        }
        // If no variations exist, create a default one from base price
        if (empty($variations) && !empty($values['price'])) {
            $variations[] = [
                'type' => 'ITEM_VARIATION',
                'id' => '#' . $post_type . '_' . $postID . '_var_default',
                'item_variation_data' => [
                    'name' => 'Regular',
                    'ordinal' => 0,
                    'pricing_type' => 'FIXED_PRICING',
                    'price_money' => [
                        'amount' => intval($values['price'] * 100),
                        'currency' => 'CAD'
                    ]
                ]
            ];
        }
        return $variations;
    }
    /**
     * Get variation mapping for post type
@@ -1616,13 +1526,9 @@
            ];
        }
        // Generate username from email
        $username = sanitize_user(current(explode('@', $email)));
        $username = $this->generateUniqueUsername($username);
        // Create user account without password (they'll set it via email)
        $user_id = wp_create_user(
            $username,
            $email,
            wp_generate_password(20, true, true), // Temporary random password
            $email
        );
@@ -1637,7 +1543,7 @@
        // Set user role (assuming you have a customer role defined)
        $user = new \WP_User($user_id);
        $user->set_role(BASE.'foodie'); // Or whatever role
        $user->set_role(BASE.'foodie');
        // Generate password reset key
        $reset_key = get_password_reset_key($user);
@@ -1653,7 +1559,7 @@
        // Link to Square customer if exists
        $square_customer_id = $this->getOrCreateSquareCustomer([
            'email' => $email,
            'name' => $username
            'name' => $email
        ]);
        if ($square_customer_id) {
@@ -1668,22 +1574,6 @@
    }
    /**
     * Generate unique username
     */
    private function generateUniqueUsername(string $base): string
    {
        $username = $base;
        $counter = 1;
        while (username_exists($username)) {
            $username = $base . $counter;
            $counter++;
        }
        return $username;
    }
    /**
     * Send welcome email with password setup
     */
    private function sendWelcomeEmail(\WP_User $user, string $reset_key): void
@@ -1699,7 +1589,7 @@
            "%s\n\n" .
            "Once you've set your password, you can:\n" .
            "- View your order history\n" .
            "- Save your favorite items\n" .
//          "- Save your favorite items\n" .
            "- Speed up checkout with saved payment methods\n\n" .
            "If you didn't create this account, please ignore this email.\n\n" .
            "Thanks,\n",
@@ -1715,49 +1605,6 @@
        );
    }
    /**
     * Track user login for security
     */
    public function trackUserLogin(string $user_login, \WP_User $user): void
    {
        // Check if user has Square integration
        $role = jvbUserRole($user->ID);
        $registrar = Registrar::getInstance($role);
        if ($registrar) {
            $config = $registrar->getIntegration($this->service_name);
            if ($config->isCustomer()) {
                $login_count = (int)get_user_meta($user->ID, BASE . '_square_login_count', true);
                $login_count++;
                update_user_meta($user->ID, BASE . '_square_login_count', $login_count);
                update_user_meta($user->ID, BASE . '_square_last_login', current_time('mysql'));
                // Check if password reset is needed
                if ($login_count % self::PASSWORD_RESET_INTERVAL === 0) {
                    $this->schedulePasswordReset($user->ID);
                }
            }
        }
    }
    /**
     * Schedule password reset for security
     */
    private function schedulePasswordReset(int $user_id): void
    {
        update_user_meta($user_id, BASE . '_square_password_reset_required', true);
        // Send notification
        $user = get_user_by('ID', $user_id);
        if ($user) {
            JVB()->email()->sendEmail(
                $user->user_email,
                '['.get_bloginfo('name').'] Security Code',
                'For your security, enter this code to continue accessing your account and saved payment methods.',
            );
        }
    }
    /******************************************************************
     * WEBHOOK HANDLING
     ******************************************************************/
@@ -3643,4 +3490,10 @@
        }, $posts->posts);
    }
    public function formatPrice(string|float|int $priceValue):int
    {
        //Convert dollars to cents
        return intval(floatval($priceValue) * 100);
    }
}
inc/managers/queue/Processor.php
@@ -218,17 +218,17 @@
        $op->errorMessage = $e->getMessage();
        JVB()->error()->log(
            '[Queue]:processOne',
            $e->getMessage(),
            [
                'operation_id' => $op->id,
                'type'         => $op->type,
                'user_id'      => $op->userId,
                'retries'      => $op->retries,
            ],
            $op->outcome === 'failed_permanent' ? 'critical' : 'warning'
        );
//      JVB()->error()->log(
//          '[Queue]:processOne',
//          $e->getMessage(),
//          [
//              'operation_id' => $op->id,
//              'type'         => $op->type,
//              'user_id'      => $op->userId,
//              'retries'      => $op->retries,
//          ],
//          $op->outcome === 'failed_permanent' ? 'critical' : 'warning'
//      );
    }
    private function calculateBackoff(int $attempt): string
inc/managers/queue/executors/IntegrationExecutor.php
@@ -86,9 +86,6 @@
     */
    private function parseOperationType(string $type): array
    {
        // Remove BASE prefix if present (e.g. 'jvb_helcim_sync_to' → 'helcim_sync_to')
        $type = str_replace(BASE, '', $type);
        $pos = strpos($type, '_');
        if ($pos === false) {
            throw new Exception("Invalid integration operation type: {$type}");
@@ -103,21 +100,12 @@
    /**
     * Resolve integration instance, optionally for a specific user
     */
    private function resolveIntegration(string $serviceName, int $userId): ?object
    private function resolveIntegration(string $serviceName, ?int $userId = null): ?object
    {
        if (!isset($this->integrationCache[$serviceName])) {
            $this->integrationCache[$serviceName] = JVB()->connect($serviceName);
        if ($userId === 0) {
            $userId = null;
        }
        $integration = $this->integrationCache[$serviceName];
        // If operation has a user context, re-instantiate for that user
        if ($integration && $userId) {
            $class = get_class($integration);
            return new $class($userId);
        }
        return $integration;
        return JVB()->connect($serviceName, $userId);
    }
    /*****************************************************************
@@ -137,6 +125,11 @@
            return new Result(outcome: 'success', result: ['synced' => [], 'message' => 'No items to sync']);
        }
        if ($integration->hasBatchUpdate()) {
            $result = $integration->updateBatchToService($data['items']);
            return new Result(outcome: $result['outcome'], result: $result['result']);
        }
        foreach ($items as $postID) {
            try {
                $result = $integration->syncPostToService((int)$postID);
inc/registrar/Registrar.php
@@ -680,8 +680,8 @@
    public static function withIntegration(string $integration, ?string $type = null):array
    {
        self::ensureInstanced();
        if (!Site::has($integration)) {
        Site::getInstance();
        if (!Site::hasIntegration($integration)) {
            error_log('[Registrar]::withIntegration Integration not available to fetch: '.$integration);
            return [];
        }
@@ -690,7 +690,7 @@
            if (!is_null($type) && $inst->type !== $type) {
                return false;
            }
            return array_key_exists($integration, $this->integrationConfigs);
            return array_key_exists($integration, $inst->integrationConfigs);
        }));
    }
inc/registrar/config/Integration.php
@@ -27,6 +27,7 @@
     * @var bool whether this is a customer role, used if this is a user-based Registrar
     */
    protected bool $isCustomer;
    protected ?string $category = null;
@@ -73,6 +74,14 @@
        return $this->content_type;
    }
    public function setCategory(string $category):void
    {
        $check = Registrar::getInstance($category);
        if ($check) {
            $this->category = $check->getBased();
        }
    }
    public function setInitial(bool $set):self
    {
        $this->initial = $set;
inc/registrar/helpers/AddIntegrationFields.php
@@ -73,6 +73,7 @@
                'type'  => 'true_false',
                'label' => 'Share To '.$this->allowed[$this->service_name],
                'section'   => 'sync',
                'default'   => 1
            ]
        ];
        if ($this->config->getUpdate()){
@@ -84,7 +85,8 @@
                    'field' => 'share_to_'.$this->service_name,
                    'value' => 1,
                    'operator'  => '=='
                ]
                ],
                'default'   => 1,
            ];
            $fields["_{$this->service_name}_item_id"] = [
@@ -128,7 +130,8 @@
                    'field' => 'share_to_'.$this->service_name,
                    'operator' => '==',
                    'value' => 1
                ]
                ],
                'default'   => 1,
            ];
        }
        $additional = JVB()->connect($this->service_name)->getAdditionalFields($this->config->getContentType());