Plugin Directory

Changeset 3450989


Ignore:
Timestamp:
01/31/2026 12:29:07 PM (3 weeks ago)
Author:
ripplestep
Message:

Version 2.0.1

Location:
sync-engine-for-intercom/trunk
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • sync-engine-for-intercom/trunk/README.txt

    r3420647 r3450989  
    77WC requires at least: 8.0
    88WC tested up to: 10.4
    9 Stable tag: 1.0.3
     9Stable tag: 2.0.1
    1010License: GPLv2 or later
    1111License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    2020This plugin is ideal for SaaS, membership sites, and WooCommerce stores that want accurate Intercom user data without custom development or expensive automation tools.
    2121
     22= Demo Video =
     23
     24See Sync Engine in action—connect WordPress and WooCommerce to Intercom, sync users, events, and tags.
     25
     26[youtube https://www.youtube.com/watch?v=Gz2w-_S4aeA]
     27
    2228= Intercom Integration for WordPress =
    2329
    24 Sync Engine is a complete Intercom integration for WordPress that seamlessly connects your site with Intercom. It automatically syncs WordPress users, updates Intercom profiles when data changes, and sends WordPress events, tags, and metadata to Intercom.
     30Sync Engine is a complete [WordPress Intercom Integration](https://ripplestep.com/wordpress-intercom-integration/) for WordPress that seamlessly connects your site with Intercom. It automatically syncs WordPress users, updates Intercom profiles when data changes, and sends WordPress events, tags, and metadata to Intercom.
    2531
    2632Whether you're running a membership site, LMS platform, WooCommerce store, or any WordPress site, this plugin provides a robust Intercom API WordPress integration that keeps your customer data synchronized in real-time.
     
    3339* **Real-Time Event Tracking**: Send WordPress events to Intercom automatically. Track user registrations, logins, profile updates, WooCommerce orders, cart additions, and custom events. All events are synced to Intercom for powerful segmentation and automation.
    3440
    35 * **WooCommerce Integration**: Full WooCommerce Intercom integration support. Track order events, cart activity, customer lifetime value, and purchase behavior. Sync WooCommerce customer data to Intercom custom attributes.
     41* **WooCommerce Integration**: Full [WooCommerce Intercom Integration](https://ripplestep.com/intercom-woocommerce/) support. Track order events, cart activity, customer lifetime value, and purchase behavior. Sync WooCommerce customer data to Intercom custom attributes.
    3642
    3743* **Custom Events Support**: Track any custom WordPress event and send it to Intercom. Use the simple API to track custom actions, milestones, or any user activity you want to monitor in Intercom.
     
    207213= Is this plugin free? =
    208214
    209 Yes, Sync Engine is free to use. Advanced features are offered in premium version.
     215Yes, Sync Engine is free to use. Advanced features are offered in premium version. For support or questions, use our [contact form](https://ripplestep.com/contact/).
    210216
    211217= Does this work with WooCommerce? =
    212218
    213 Yes, this plugin includes full WooCommerce Intercom integration. It syncs WooCommerce customers to Intercom, tracks order events, monitors cart activity, and sends purchase data to Intercom for segmentation and automation.
     219Yes, this plugin includes full [WooCommerce Intercom Integration](https://ripplestep.com/intercom-woocommerce/). It syncs WooCommerce customers to Intercom, tracks order events, monitors cart activity, and sends purchase data to Intercom for segmentation and automation.
    214220
    215221= How do I sync custom WordPress user metadata to Intercom? =
     
    258264
    259265= Changelog =
     266
     267= 2.0.1 =
     268* Async Role Tag Added
     269* HPOS Compatiblity
     270* Reduced API Calls for User Sync And Event Sync
     271* Used Rate Limiting along With Instant Async Call
     272
    260273
    261274= 1.0.3 =
  • sync-engine-for-intercom/trunk/includes/api/class-intercom-contacts.php

    r3420647 r3450989  
    152152            return false;
    153153        }
     154    }
     155
     156    /**
     157     * Get contact ID with caching to minimize API calls.
     158     *
     159     * This method:
     160     * 1. Checks WordPress user meta for cached contact_id
     161     * 2. If not cached, searches Intercom by external_id or email
     162     * 3. Caches the contact_id in user meta for future lookups
     163     *
     164     * @since 1.0.0
     165     * @param int         $user_id       WordPress user ID.
     166     * @param string|null $external_id   Optional external ID (e.g., 'wp_user_123').
     167     * @param string|null $email         Optional email address.
     168     * @param bool        $force_refresh Force refresh from Intercom (default: false).
     169     * @return string|false Contact ID or false if not found.
     170     */
     171    public function get_cached_contact_id( int $user_id, ?string $external_id = null, ?string $email = null, bool $force_refresh = false ): string|false {
     172        $prefix = 'rpplstp_iws';
     173
     174        // Check cache first (unless force refresh).
     175        if ( ! $force_refresh ) {
     176            $cached_contact_id = get_user_meta( $user_id, $prefix . '_intercom_contact_id', true );
     177            if ( ! empty( $cached_contact_id ) ) {
     178                $this->logger->debug( 'Using cached contact_id', array(
     179                    'user_id' => $user_id,
     180                    'contact_id' => $cached_contact_id,
     181                ) );
     182                return $cached_contact_id;
     183            }
     184        }
     185
     186        // Cache miss or force refresh - lookup from Intercom.
     187        $contact_id = null;
     188
     189        // Try external_id first (most reliable).
     190        if ( ! empty( $external_id ) ) {
     191            $contact_id = $this->find_by_external_id( $external_id );
     192            if ( $contact_id ) {
     193                $this->logger->debug( 'Contact found by external_id', array(
     194                    'user_id' => $user_id,
     195                    'external_id' => $external_id,
     196                    'contact_id' => $contact_id,
     197                ) );
     198            }
     199        }
     200
     201        // Fallback to email search.
     202        if ( ! $contact_id && ! empty( $email ) ) {
     203            $contact_id = $this->find_by_email( $email );
     204            if ( $contact_id ) {
     205                $this->logger->debug( 'Contact found by email', array(
     206                    'user_id' => $user_id,
     207                    'email' => $email,
     208                    'contact_id' => $contact_id,
     209                ) );
     210            }
     211        }
     212
     213        // Cache the result (even if false, to avoid repeated lookups for non-existent contacts).
     214        if ( $contact_id ) {
     215            update_user_meta( $user_id, $prefix . '_intercom_contact_id', $contact_id );
     216            $this->logger->debug( 'Cached contact_id in user meta', array(
     217                'user_id' => $user_id,
     218                'contact_id' => $contact_id,
     219            ) );
     220        }
     221
     222        return $contact_id ?: false;
     223    }
     224
     225    /**
     226     * Clear cached contact ID for a user.
     227     *
     228     * Call this when user email changes or contact needs to be re-synced.
     229     *
     230     * @since 1.0.0
     231     * @param int $user_id WordPress user ID.
     232     * @return bool True on success, false on failure.
     233     */
     234    public function clear_cached_contact_id( int $user_id ): bool {
     235        $prefix = 'rpplstp_iws';
     236        return delete_user_meta( $user_id, $prefix . '_intercom_contact_id' );
    154237    }
    155238
  • sync-engine-for-intercom/trunk/includes/api/class-intercom-tags.php

    r3420647 r3450989  
    2121use Intercom\Tags\Types\Tag;
    2222use Intercom\Types\TagList;
     23use Intercom\Contacts\Requests\ListTagsAttachedToContactRequest;
    2324
    2425/**
     
    116117            $tag_list = $this->api->api_call( 'tags', 'list' );
    117118           
     119            // Debug: Log raw response type and structure.
     120            $this->logger->debug( 'Raw tag list response', array(
     121                'type' => gettype( $tag_list ),
     122                'class' => is_object( $tag_list ) ? get_class( $tag_list ) : 'not_object',
     123                'has_getTags' => is_object( $tag_list ) && method_exists( $tag_list, 'getTags' ),
     124                'has_tags_prop' => is_object( $tag_list ) && isset( $tag_list->tags ),
     125                'has_getData' => is_object( $tag_list ) && method_exists( $tag_list, 'getData' ),
     126                'is_array' => is_array( $tag_list ),
     127            ) );
     128           
    118129            if ( ! $tag_list ) {
     130                $this->logger->warning( 'Tag list returned empty', array( 'searching_for' => $tag_name ) );
    119131                return false;
    120132            }
    121133
    122             // Get tags from TagList.
     134            // Get tags from TagList - try multiple access patterns.
    123135            $tags = array();
     136           
     137            // Pattern 1: getTags() method
    124138            if ( method_exists( $tag_list, 'getTags' ) ) {
    125139                $tags = $tag_list->getTags();
    126             } elseif ( isset( $tag_list->tags ) ) {
     140                $this->logger->debug( 'Using getTags() method', array( 'count' => is_array( $tags ) ? count( $tags ) : 0 ) );
     141            }
     142            // Pattern 2: getData() method (paginated response)
     143            elseif ( method_exists( $tag_list, 'getData' ) ) {
     144                $tags = $tag_list->getData();
     145                $this->logger->debug( 'Using getData() method', array( 'count' => is_array( $tags ) ? count( $tags ) : 0 ) );
     146            }
     147            // Pattern 3: tags property
     148            elseif ( isset( $tag_list->tags ) ) {
    127149                $tags = $tag_list->tags;
    128             } elseif ( is_array( $tag_list ) ) {
     150                $this->logger->debug( 'Using tags property', array( 'count' => is_array( $tags ) ? count( $tags ) : 0 ) );
     151            }
     152            // Pattern 4: data property (common in Intercom responses)
     153            elseif ( isset( $tag_list->data ) ) {
     154                $tags = $tag_list->data;
     155                $this->logger->debug( 'Using data property', array( 'count' => is_array( $tags ) ? count( $tags ) : 0 ) );
     156            }
     157            // Pattern 5: Direct array
     158            elseif ( is_array( $tag_list ) ) {
    129159                $tags = $tag_list;
    130             }
     160                $this->logger->debug( 'Using direct array', array( 'count' => count( $tags ) ) );
     161            }
     162
     163            // Collect all tag names for debugging.
     164            $all_tag_names = array();
     165            foreach ( $tags as $tag ) {
     166                if ( method_exists( $tag, 'getName' ) ) {
     167                    $all_tag_names[] = $tag->getName();
     168                } elseif ( isset( $tag->name ) ) {
     169                    $all_tag_names[] = $tag->name;
     170                } elseif ( is_array( $tag ) && isset( $tag['name'] ) ) {
     171                    $all_tag_names[] = $tag['name'];
     172                }
     173            }
     174
     175            $this->logger->debug( 'Searching for tag in Intercom', array(
     176                'searching_for' => $tag_name,
     177                'total_tags' => count( $all_tag_names ),
     178                'all_tags' => $all_tag_names,
     179            ) );
    131180
    132181            // Search for tag by name (case-insensitive).
     
    134183            foreach ( $tags as $tag ) {
    135184                $current_name = '';
     185                $current_id = '';
     186               
     187                // Extract name
    136188                if ( method_exists( $tag, 'getName' ) ) {
    137189                    $current_name = $tag->getName();
    138190                } elseif ( isset( $tag->name ) ) {
    139191                    $current_name = $tag->name;
     192                } elseif ( is_array( $tag ) && isset( $tag['name'] ) ) {
     193                    $current_name = $tag['name'];
     194                }
     195               
     196                // Extract ID
     197                if ( method_exists( $tag, 'getId' ) ) {
     198                    $current_id = $tag->getId();
     199                } elseif ( isset( $tag->id ) ) {
     200                    $current_id = $tag->id;
     201                } elseif ( is_array( $tag ) && isset( $tag['id'] ) ) {
     202                    $current_id = $tag['id'];
    140203                }
    141204
    142205                if ( strtolower( $current_name ) === $tag_name_lower ) {
    143                     if ( method_exists( $tag, 'getId' ) ) {
    144                         return $tag->getId();
    145                     } elseif ( isset( $tag->id ) ) {
    146                         return $tag->id;
    147                     }
    148                 }
    149             }
     206                    return $current_id ?: false;
     207                }
     208            }
     209
     210            $this->logger->warning( 'Tag not found in Intercom', array(
     211                'searching_for' => $tag_name,
     212                'available_tags' => $all_tag_names,
     213            ) );
    150214
    151215            return false;
     
    205269        }
    206270
    207         try {
    208             // Find the tag ID.
    209             $tag_id = $this->find_tag_by_name( $tag_name );
    210             if ( ! $tag_id ) {
    211                 // Tag doesn't exist, nothing to untag.
    212                 return true;
    213             }
    214 
    215             // Untag the contact.
    216             $request = new UntagContactRequest( array(
    217                 'contactId' => $contact_id,
    218                 'tagId' => $tag_id,
     271    try {
     272        // Find the tag ID.
     273        $tag_id = $this->find_tag_by_name( $tag_name );
     274        if ( ! $tag_id ) {
     275            // Tag doesn't exist in Intercom, nothing to untag.
     276            $this->logger->debug( 'Tag not found in Intercom, skipping untag', array(
     277                'contact_id' => $contact_id,
     278                'tag_name' => $tag_name,
    219279            ) );
    220280
    221             $this->api->api_call( 'tags', 'untagContact', $request );
    222             $this->logger->debug( 'Contact untagged successfully', array( 'contact_id' => $contact_id, 'tag_name' => $tag_name ) );
     281
    223282            return true;
     283        }
     284
     285        $this->logger->debug( 'Found tag for untagging', array(
     286            'tag_name' => $tag_name,
     287            'tag_id' => $tag_id,
     288        ) );
     289
     290        // Untag the contact.
     291        $request = new UntagContactRequest( array(
     292            'contactId' => $contact_id,
     293            'tagId' => $tag_id,
     294        ) );
     295
     296        $this->api->api_call( 'tags', 'untagContact', $request );
     297        $this->logger->info( 'Contact untagged successfully', array(
     298            'contact_id' => $contact_id,
     299            'tag_name' => $tag_name,
     300            'tag_id' => $tag_id,
     301        ) );
     302        return true;
    224303        } catch ( \Exception $e ) {
    225304            $this->logger->error( 'Failed to untag contact', array( 'contact_id' => $contact_id, 'tag_name' => $tag_name, 'error' => $e->getMessage() ) );
     
    229308
    230309    /**
     310     * Get tags attached to a contact.
     311     *
     312     * This method is used ONLY for first-time sync to discover existing role tags.
     313     * After first sync, role tracking uses local user meta to avoid API calls.
     314     *
     315     * @since 1.0.0
     316     * @param string $contact_id Contact ID.
     317     * @return array<string> Array of tag names, or empty array on failure.
     318     */
     319    private function get_contact_tags( string $contact_id ): array {
     320        if ( empty( $contact_id ) ) {
     321            return array();
     322        }
     323
     324        try {
     325            $request = new ListTagsAttachedToContactRequest( array( 'contactId' => $contact_id ) );
     326            $tag_list = $this->api->api_call( 'contacts', 'listAttachedTags', $request );
     327
     328            if ( ! $tag_list ) {
     329                return array();
     330            }
     331
     332            // Extract tag names from TagList - try multiple access patterns.
     333            $tag_names = array();
     334            $tags = array();
     335           
     336            // Pattern 1: getTags() method
     337            if ( method_exists( $tag_list, 'getTags' ) ) {
     338                $tags = $tag_list->getTags();
     339            }
     340            // Pattern 2: getData() method (paginated response)
     341            elseif ( method_exists( $tag_list, 'getData' ) ) {
     342                $tags = $tag_list->getData();
     343            }
     344            // Pattern 3: tags property
     345            elseif ( isset( $tag_list->tags ) ) {
     346                $tags = $tag_list->tags;
     347            }
     348            // Pattern 4: data property (common in Intercom responses)
     349            elseif ( isset( $tag_list->data ) ) {
     350                $tags = $tag_list->data;
     351            }
     352            // Pattern 5: Direct array
     353            elseif ( is_array( $tag_list ) ) {
     354                $tags = $tag_list;
     355            }
     356
     357            foreach ( $tags as $tag ) {
     358                if ( method_exists( $tag, 'getName' ) ) {
     359                    $tag_names[] = $tag->getName();
     360                } elseif ( isset( $tag->name ) ) {
     361                    $tag_names[] = $tag->name;
     362                } elseif ( is_array( $tag ) && isset( $tag['name'] ) ) {
     363                    $tag_names[] = $tag['name'];
     364                }
     365            }
     366
     367            return $tag_names;
     368        } catch ( \Exception $e ) {
     369            $this->logger->error( 'Failed to get contact tags', array(
     370                'contact_id' => $contact_id,
     371                'error' => $e->getMessage(),
     372            ) );
     373            return array();
     374        }
     375    }
     376
     377    /**
     378     * Clear cached tag sync data for a user.
     379     *
     380     * Forces next sync to fetch existing tags from Intercom.
     381     * Useful for fixing sync issues or migrating users to new caching system.
     382     *
     383     * @since 1.0.0
     384     * @param int $user_id WordPress user ID.
     385     * @return bool True on success, false on failure.
     386     */
     387    public function clear_tag_sync_cache( int $user_id ): bool {
     388        $prefix = 'rpplstp_iws';
     389        $result = delete_user_meta( $user_id, $prefix . '_synced_roles' );
     390       
     391        if ( $result ) {
     392            $this->logger->info( 'Tag sync cache cleared for user', array( 'user_id' => $user_id ) );
     393        }
     394       
     395        return $result;
     396    }
     397
     398    /**
     399     * Clear tag sync cache for all users.
     400     *
     401     * Use this to force all users to re-sync with Intercom on next sync.
     402     * Useful when migrating to new tag sync system.
     403     *
     404     * @since 1.0.0
     405     * @return int Number of users cleared.
     406     */
     407    public function clear_all_tag_sync_caches(): int {
     408        global $wpdb;
     409        $prefix = 'rpplstp_iws';
     410       
     411        $result = $wpdb->delete(
     412            $wpdb->usermeta,
     413            array( 'meta_key' => $prefix . '_synced_roles' ),
     414            array( '%s' )
     415        );
     416       
     417        $count = $result ? (int) $result : 0;
     418        $this->logger->info( 'Tag sync cache cleared for all users', array( 'count' => $count ) );
     419       
     420        return $count;
     421    }
     422
     423    /**
    231424     * Sync tags for a contact based on user data.
     425     *
     426     * This method ensures that role tags are properly synchronized by:
     427     * - Tracking last synced roles in WordPress user meta
     428     * - Only making API calls for actual changes (minimizes API usage)
     429     * - Removing old role tags that are no longer valid
     430     * - Adding new role tags for current roles
    232431     *
    233432     * @since 1.0.0
     
    259458
    260459        // Sync role tags if enabled.
    261         if ( in_array( 'role', $tags_to_sync, true ) && ! empty( $user->roles ) ) {
    262             foreach ( $user->roles as $role ) {
    263                 // Use the role key (slug) with wp_role prefix.
    264                 $tag_name = 'wp_role_' . $role;
     460        if ( in_array( 'role', $tags_to_sync, true ) ) {
     461            // Get current WordPress roles.
     462            $current_roles = ! empty( $user->roles ) ? $user->roles : array();
     463            sort( $current_roles ); // Sort for consistent comparison.
     464           
     465            // Get last synced roles from user meta.
     466            $last_synced_roles = get_user_meta( $user_id, $prefix . '_synced_roles', true );
     467            $is_first_sync = empty( $last_synced_roles );
     468           
     469            if ( ! is_array( $last_synced_roles ) ) {
     470                $last_synced_roles = array();
     471            }
     472
     473            // On first sync, fetch existing role tags from Intercom to properly identify stale tags.
     474            // This is a one-time cost per user to ensure accurate tag cleanup.
     475            if ( $is_first_sync ) {
     476                $this->logger->info( 'First-time tag sync, fetching existing tags from Intercom', array(
     477                    'user_id' => $user_id,
     478                    'contact_id' => $contact_id,
     479                ) );
     480               
     481                $existing_tags = $this->get_contact_tags( $contact_id );
     482               
     483                // Extract existing role tags (tags starting with 'wp_role_').
     484                foreach ( $existing_tags as $tag ) {
     485                    if ( strpos( $tag, 'wp_role_' ) === 0 ) {
     486                        $role = str_replace( 'wp_role_', '', $tag );
     487                        $last_synced_roles[] = $role;
     488                    }
     489                }
     490               
     491                $this->logger->debug( 'Discovered existing role tags', array(
     492                    'user_id' => $user_id,
     493                    'existing_roles' => $last_synced_roles,
     494                ) );
     495            }
     496           
     497            sort( $last_synced_roles ); // Sort for consistent comparison.
     498
     499            // Check if roles have changed.
     500            if ( $current_roles === $last_synced_roles && ! $is_first_sync ) {
     501                // No changes, skip API calls (but not on first sync).
     502                $this->logger->debug( 'Role tags unchanged, skipping sync', array(
     503                    'user_id' => $user_id,
     504                    'roles' => $current_roles,
     505                ) );
     506                return true;
     507            }
     508
     509            // Determine which roles were added and removed.
     510            $roles_to_remove = array_diff( $last_synced_roles, $current_roles );
     511            $roles_to_add = array_diff( $current_roles, $last_synced_roles );
     512
     513            // Remove old role tags.
     514            foreach ( $roles_to_remove as $old_role ) {
     515                $tag_name = 'wp_role_' . $old_role;
     516                $this->logger->debug( 'Removing old role tag', array(
     517                    'contact_id' => $contact_id,
     518                    'user_id' => $user_id,
     519                    'role' => $old_role,
     520                    'tag' => $tag_name,
     521                ) );
     522                if ( ! $this->untag_contact( $contact_id, $tag_name ) ) {
     523                    $success = false;
     524                    $this->logger->warning( 'Failed to remove old role tag', array(
     525                        'contact_id' => $contact_id,
     526                        'tag' => $tag_name,
     527                    ) );
     528                }
     529            }
     530
     531            // Add new role tags.
     532            foreach ( $roles_to_add as $new_role ) {
     533                $tag_name = 'wp_role_' . $new_role;
     534                $this->logger->debug( 'Adding new role tag', array(
     535                    'contact_id' => $contact_id,
     536                    'user_id' => $user_id,
     537                    'role' => $new_role,
     538                    'tag' => $tag_name,
     539                ) );
    265540                if ( ! $this->tag_contact( $contact_id, $tag_name ) ) {
    266541                    $success = false;
     542                    $this->logger->warning( 'Failed to add new role tag', array(
     543                        'contact_id' => $contact_id,
     544                        'tag' => $tag_name,
     545                    ) );
     546                }
     547            }
     548
     549            // Update the stored synced roles if successful.
     550            if ( $success ) {
     551                update_user_meta( $user_id, $prefix . '_synced_roles', $current_roles );
     552               
     553                // Log summary of changes.
     554                if ( ! empty( $roles_to_remove ) || ! empty( $roles_to_add ) ) {
     555                    $this->logger->info( 'Role tags synchronized', array(
     556                        'contact_id' => $contact_id,
     557                        'user_id' => $user_id,
     558                        'removed_roles' => array_values( $roles_to_remove ),
     559                        'added_roles' => array_values( $roles_to_add ),
     560                    ) );
    267561                }
    268562            }
  • sync-engine-for-intercom/trunk/includes/core/class-action-scheduler-handler.php

    r3420647 r3450989  
    101101        }
    102102
    103         // Register action hooks for background processing.
    104         add_action( self::PREFIX . '_sync_user', array( $this, 'process_user_sync' ), 10, 1 );
    105         add_action( self::PREFIX . '_sync_event', array( $this, 'process_event_sync' ), 10, 2 );
    106 
    107         // Mark hooks as initialized.
    108         self::$hooks_initialized = true;
     103    // Register action hooks for background processing.
     104    add_action( self::PREFIX . '_sync_user', array( $this, 'process_user_sync' ), 10, 1 );
     105    add_action( self::PREFIX . '_sync_event', array( $this, 'process_event_sync' ), 10, 2 );
     106    add_action( self::PREFIX . '_sync_tags', array( $this, 'process_tag_sync' ), 10, 2 );
     107
     108    // Mark hooks as initialized.
     109    self::$hooks_initialized = true;
    109110    }
    110111
     
    143144        }
    144145
    145         $timestamp = time() + $delay;
    146         $action_id = as_schedule_single_action(
    147             $timestamp,
     146        // Dispatch the action using the appropriate method.
     147        $action_id = $this->dispatch_action(
    148148            self::PREFIX . '_sync_user',
    149149            array( $user_id ),
    150             self::ACTION_GROUP
     150            $delay
    151151        );
    152152
     
    156156                'action_id' => $action_id,
    157157                'delay' => $delay,
     158                'type' => $delay > 0 ? 'scheduled' : 'async',
    158159            ) );
    159160        } else {
     
    189190        }
    190191
    191         $timestamp = time() + $delay;
    192         $action_id = as_schedule_single_action(
    193             $timestamp,
     192        // Dispatch the action using the appropriate method.
     193        $action_id = $this->dispatch_action(
    194194            self::PREFIX . '_sync_event',
    195195            array( $event_name, $event_data ),
    196             self::ACTION_GROUP
     196            $delay
    197197        );
    198198
     
    202202                'action_id' => $action_id,
    203203                'delay' => $delay,
     204                'type' => $delay > 0 ? 'scheduled' : 'async',
    204205            ) );
    205206        } else {
     
    207208        }
    208209
    209         return $action_id;
    210     }
    211 
    212 
    213     /**
    214      * Process user sync action (called by Action Scheduler).
     210    return $action_id;
     211}
     212
     213/**
     214 * Schedule tag sync action.
     215 *
     216 * @since 1.0.0
     217 * @param string $contact_id Intercom contact ID.
     218 * @param int    $user_id    WordPress user ID.
     219 * @param int    $delay      Optional delay in seconds before processing (default: 0).
     220 * @return int|false Action ID on success, false on failure.
     221 */
     222public function schedule_tag_sync( string $contact_id, int $user_id, int $delay = 0 ): int|false {
     223    if ( ! $this->is_available() ) {
     224        $this->logger->warning( 'Action Scheduler not available, skipping tag sync', array(
     225            'contact_id' => $contact_id,
     226            'user_id' => $user_id,
     227        ) );
     228        return false;
     229    }
     230
     231    // Validate contact_id (critical dependency).
     232    if ( empty( $contact_id ) ) {
     233        $this->logger->error( 'Cannot schedule tag sync: contact_id is empty', array( 'user_id' => $user_id ) );
     234        return false;
     235    }
     236
     237    // Check if tag sync is enabled.
     238    $enable_tags_sync = get_option( self::PREFIX . '_enable_tags_sync', '0' );
     239    if ( '1' !== $enable_tags_sync ) {
     240        $this->logger->debug( 'Tag sync disabled, skipping scheduling', array( 'user_id' => $user_id ) );
     241        return false;
     242    }
     243
     244    // Check rate limit and add delay if needed.
     245    $rate_limit_delay = $this->rate_limiter->get_delay();
     246    if ( $rate_limit_delay > 0 ) {
     247        $delay = max( $delay, $rate_limit_delay );
     248        $this->logger->debug( 'Rate limit delay added to tag sync', array(
     249            'contact_id' => $contact_id,
     250            'user_id' => $user_id,
     251            'delay' => $delay,
     252        ) );
     253    }
     254
     255    // Dispatch the action using the appropriate method.
     256    $action_id = $this->dispatch_action(
     257        self::PREFIX . '_sync_tags',
     258        array( $contact_id, $user_id ),
     259        $delay
     260    );
     261
     262    if ( $action_id ) {
     263        $this->logger->info( 'Tag sync scheduled', array(
     264            'contact_id' => $contact_id,
     265            'user_id' => $user_id,
     266            'action_id' => $action_id,
     267            'delay' => $delay,
     268            'type' => $delay > 0 ? 'scheduled' : 'async',
     269        ) );
     270    } else {
     271        $this->logger->error( 'Failed to schedule tag sync', array(
     272            'contact_id' => $contact_id,
     273            'user_id' => $user_id,
     274        ) );
     275    }
     276
     277    return $action_id;
     278}
     279
     280
     281/**
     282 * Process user sync action (called by Action Scheduler).
    215283     *
    216284     * @since 1.0.0
     
    348416            ) );
    349417
    350             // Fire action for error handling.
    351             do_action( self::PREFIX . '_event_sync_failed', $event_name, $event_data, $e );
     418        // Fire action for error handling.
     419        do_action( self::PREFIX . '_event_sync_failed', $event_name, $event_data, $e );
     420    }
     421}
     422
     423/**
     424 * Process tag sync action (called by Action Scheduler).
     425 *
     426 * @since 1.0.0
     427 * @param string $contact_id Intercom contact ID.
     428 * @param int    $user_id    WordPress user ID.
     429 * @return void
     430 */
     431public function process_tag_sync( string $contact_id, int $user_id ): void {
     432    $this->logger->info( 'Processing scheduled tag sync', array(
     433        'contact_id' => $contact_id,
     434        'user_id' => $user_id,
     435    ) );
     436
     437    // Validate contact_id again (in case it was lost).
     438    if ( empty( $contact_id ) ) {
     439        $this->logger->error( 'Tag sync skipped: contact_id is empty', array( 'user_id' => $user_id ) );
     440        return;
     441    }
     442
     443    // Check if connection is available.
     444    $access_token = RPPLSTP_IWS_Admin_Settings::get_access_token();
     445    if ( empty( $access_token ) ) {
     446        $this->logger->warning( 'Scheduled tag sync skipped: No access token', array(
     447            'contact_id' => $contact_id,
     448            'user_id' => $user_id,
     449        ) );
     450        return;
     451    }
     452
     453    // Check if tag sync is still enabled.
     454    $enable_tags_sync = get_option( self::PREFIX . '_enable_tags_sync', '0' );
     455    if ( '1' !== $enable_tags_sync ) {
     456        $this->logger->debug( 'Tag sync disabled, skipping scheduled action', array( 'user_id' => $user_id ) );
     457        return;
     458    }
     459
     460    // Check rate limit before processing.
     461    if ( ! $this->rate_limiter->can_make_request() ) {
     462        $delay = $this->rate_limiter->get_delay();
     463        $this->logger->warning( 'Rate limit reached, rescheduling tag sync', array(
     464            'contact_id' => $contact_id,
     465            'user_id' => $user_id,
     466            'delay' => $delay,
     467        ) );
     468        // Reschedule with delay.
     469        $this->schedule_tag_sync( $contact_id, $user_id, $delay );
     470        return;
     471    }
     472
     473    // Perform the actual tag sync.
     474    $tags = new RPPLSTP_IWS_Intercom_Tags();
     475    $result = $tags->sync_tags_for_contact( $contact_id, $user_id );
     476
     477    // Record the API request.
     478    // Note: sync_tags_for_contact() uses local tracking (user meta) to minimize API calls.
     479    // First sync: 1 call to list existing tags + calls for role changes
     480    // Subsequent syncs: Only makes API calls for actual role changes
     481    // - Remove old role tags (1 call per removed role)
     482    // - Add new role tags (1 call per added role)
     483    // If roles haven't changed, no API calls are made.
     484    // We record one request here as a simplification for rate limiting.
     485    if ( $result ) {
     486        $this->rate_limiter->record_request();
     487    }
     488
     489    if ( $result ) {
     490        $this->logger->info( 'Scheduled tag sync completed successfully', array(
     491            'contact_id' => $contact_id,
     492            'user_id' => $user_id,
     493        ) );
     494
     495        // Fire action for other plugins to hook into.
     496        do_action( self::PREFIX . '_tags_synced', $contact_id, $user_id );
     497    } else {
     498        $this->logger->error( 'Scheduled tag sync failed', array(
     499            'contact_id' => $contact_id,
     500            'user_id' => $user_id,
     501        ) );
     502
     503        // Fire action for error handling.
     504        do_action( self::PREFIX . '_tag_sync_failed', $contact_id, $user_id );
     505    }
     506}
     507
     508/**
     509 * Dispatch an action using either async or scheduled method based on delay.
     510     *
     511     * @since 1.0.0
     512     * @param string $hook  The hook name to trigger.
     513     * @param array  $args  Arguments to pass to the hook.
     514     * @param int    $delay Delay in seconds (0 for immediate).
     515     * @return int|false Action ID on success, false on failure.
     516     */
     517    private function dispatch_action( string $hook, array $args, int $delay ): int|false {
     518        // Use async action for immediate execution, scheduled action for delayed execution.
     519        if ( $delay > 0 ) {
     520            // Schedule for future execution with delay.
     521            $timestamp = time() + $delay;
     522            return as_schedule_single_action(
     523                $timestamp,
     524                $hook,
     525                $args,
     526                self::ACTION_GROUP
     527            );
     528        } else {
     529            // Enqueue for immediate async execution.
     530            return as_enqueue_async_action(
     531                $hook,
     532                $args,
     533                self::ACTION_GROUP
     534            );
    352535        }
    353536    }
     
    416599        $this->ensure_action_scheduler_loaded();
    417600
    418         // Check if the function exists (Action Scheduler is loaded).
    419         // This is the primary check - if the function exists, Action Scheduler is loaded.
    420         if ( ! function_exists( 'as_schedule_single_action' ) ) {
     601        // Check if the functions exist (Action Scheduler is loaded).
     602        // This is the primary check - if the functions exist, Action Scheduler is loaded.
     603        if ( ! function_exists( 'as_schedule_single_action' ) || ! function_exists( 'as_enqueue_async_action' ) ) {
    421604            return false;
    422605        }
  • sync-engine-for-intercom/trunk/includes/sync/class-event-sync.php

    r3420647 r3450989  
    601601        $this->ensure_user_synced( $event_data );
    602602
    603         // Try to get Intercom contact ID if we have user_id or email.
    604         // Only attempt lookup if we have at least one identifier.
    605         if ( ! empty( $event_data['user_id'] ) || ! empty( $event_data['email'] ) ) {
    606             // Prefer email lookup as it's more reliable.
    607             if ( ! empty( $event_data['email'] ) ) {
    608                 $intercom_contacts = new RPPLSTP_IWS_Intercom_Contacts();
    609                 $intercom_id = $intercom_contacts->find_by_email( $event_data['email'] );
    610                 if ( $intercom_id ) {
    611                     $event_data['intercom_id'] = $intercom_id;
    612                 }
    613             }
    614         }
     603    // Try to get Intercom contact ID using cached lookup to minimize API calls.
     604    // Only attempt lookup if we have user_id (needed for cache).
     605    if ( ! empty( $event_data['user_id'] ) ) {
     606        $intercom_contacts = new RPPLSTP_IWS_Intercom_Contacts();
     607        $intercom_id = $intercom_contacts->get_cached_contact_id(
     608            $event_data['user_id'],
     609            null, // external_id not needed here
     610            $event_data['email'] ?? null
     611        );
     612        if ( $intercom_id ) {
     613            $event_data['intercom_id'] = $intercom_id;
     614        }
     615    }
    615616
    616617        // Schedule event sync in background.
     
    695696            }
    696697
    697             // Lazy load user sync instance.
    698             if ( null === $this->user_sync ) {
    699                 $this->user_sync = new RPPLSTP_IWS_User_Sync();
    700             }
    701 
    702             // Check if user exists in Intercom.
    703             $intercom_contacts = new RPPLSTP_IWS_Intercom_Contacts();
    704             $user_email = $user->user_email;
    705             $intercom_id = $intercom_contacts->find_by_email( $user_email );
    706 
    707             if ( ! $intercom_id ) {
    708                 // User doesn't exist in Intercom, sync them now.
    709                 $this->logger->debug( 'User not found in Intercom, syncing user before event', array(
    710                     'user_id' => $user_id,
    711                     'email' => $user_email,
    712                 ) );
    713                 $this->user_sync->schedule_sync( $user_id, 0 );
    714             } else {
    715                 // User exists, but ensure their data is up-to-date by scheduling a sync.
    716                 // This ensures profile data (name, phone, avatar, etc.) is current.
    717                 $this->logger->debug( 'User exists in Intercom, ensuring profile is up-to-date before event', array(
    718                     'user_id' => $user_id,
    719                     'intercom_id' => $intercom_id,
    720                 ) );
    721                 $this->user_sync->schedule_sync( $user_id, 0 );
    722                 // Add intercom_id to event data for better event attribution.
    723                 $event_data['intercom_id'] = $intercom_id;
    724             }
     698        // Lazy load user sync instance.
     699        if ( null === $this->user_sync ) {
     700            $this->user_sync = new RPPLSTP_IWS_User_Sync();
     701        }
     702
     703        // Check if user exists in Intercom using cached lookup.
     704        $intercom_contacts = new RPPLSTP_IWS_Intercom_Contacts();
     705        $user_email = $user->user_email;
     706        $intercom_id = $intercom_contacts->get_cached_contact_id(
     707            $user_id,
     708            null, // external_id not needed here
     709            $user_email
     710        );
     711
     712        if ( ! $intercom_id ) {
     713            // User doesn't exist in Intercom, sync them now.
     714            $this->logger->debug( 'User not found in Intercom, syncing user before event', array(
     715                'user_id' => $user_id,
     716                'email' => $user_email,
     717            ) );
     718            $this->user_sync->schedule_sync( $user_id, 0 );
     719        } else {
     720            // User exists, but ensure their data is up-to-date by scheduling a sync.
     721            // This ensures profile data (name, phone, avatar, etc.) is current.
     722            $this->logger->debug( 'User exists in Intercom, ensuring profile is up-to-date before event', array(
     723                'user_id' => $user_id,
     724                'intercom_id' => $intercom_id,
     725            ) );
     726            $this->user_sync->schedule_sync( $user_id, 0 );
     727            // Add intercom_id to event data for better event attribution.
     728            $event_data['intercom_id'] = $intercom_id;
     729        }
    725730        }
    726731    }
  • sync-engine-for-intercom/trunk/includes/sync/class-user-sync.php

    r3420647 r3450989  
    244244        }
    245245
    246         try {
    247             // Try to find existing contact by externalId first (most reliable).
    248             // This prevents duplicates even if email changes.
    249             $contact_id = null;
    250             if ( ! empty( $user_data['externalId'] ) ) {
    251                 $contact_id = $this->contacts->find_by_external_id( $user_data['externalId'] );
    252                 $this->logger->debug( 'Contact lookup by externalId', array(
    253                     'user_id' => $user_id,
    254                     'externalId' => $user_data['externalId'],
    255                     'found' => ! empty( $contact_id )
     246    try {
     247        // Use cached contact lookup to minimize API calls.
     248        // This checks user meta first, only queries Intercom if not cached.
     249        $contact_id = $this->contacts->get_cached_contact_id(
     250            $user_id,
     251            $user_data['externalId'] ?? null,
     252            $user_data['email'] ?? null
     253        );
     254
     255        if ( $contact_id ) {
     256            // Update existing contact.
     257            $this->logger->debug( 'Updating existing Intercom contact', array( 'user_id' => $user_id, 'contact_id' => $contact_id ) );
     258            $result = $this->contacts->update( $contact_id, $user_data );
     259            // Ensure contact_id is in result.
     260            if ( is_array( $result ) && isset( $result['contact_id'] ) ) {
     261                $contact_id = $result['contact_id'];
     262            }
     263        } else {
     264            // Create new contact.
     265            $this->logger->debug( 'Creating new Intercom contact', array( 'user_id' => $user_id ) );
     266            $result = $this->contacts->create( $user_data );
     267            // Get contact ID from result and cache it.
     268            if ( is_array( $result ) && isset( $result['contact_id'] ) ) {
     269                $contact_id = $result['contact_id'];
     270                // Cache the newly created contact_id.
     271                update_user_meta( $user_id, 'rpplstp_iws_intercom_contact_id', $contact_id );
     272            }
     273        }
     274
     275        // Schedule tag sync asynchronously if enabled.
     276        // This ensures user sync completes quickly and tags are processed in background.
     277        if ( ! empty( $contact_id ) ) {
     278            $tag_action_id = $this->action_scheduler->schedule_tag_sync( $contact_id, $user_id, 0 );
     279            if ( $tag_action_id ) {
     280                $this->logger->debug( 'Tag sync scheduled for user', array(
     281                    'user_id' => $user_id,
     282                    'contact_id' => $contact_id,
     283                    'tag_action_id' => $tag_action_id,
    256284                ) );
    257285            }
    258 
    259             // Fallback to email search if externalId lookup didn't find a contact.
    260             // This handles legacy contacts that may not have externalId set yet.
    261             if ( ! $contact_id && ! empty( $user_data['email'] ) ) {
    262                 $contact_id = $this->contacts->find_by_email( $user_data['email'] );
    263                 $this->logger->debug( 'Contact lookup by email (fallback)', array(
    264                     'user_id' => $user_id,
    265                     'email' => $user_data['email'],
    266                     'found' => ! empty( $contact_id )
    267                 ) );
    268             }
    269 
    270             if ( $contact_id ) {
    271                 // Update existing contact.
    272                 $this->logger->debug( 'Updating existing Intercom contact', array( 'user_id' => $user_id, 'contact_id' => $contact_id ) );
    273                 $result = $this->contacts->update( $contact_id, $user_data );
    274                 // Ensure contact_id is in result.
    275                 if ( is_array( $result ) && isset( $result['contact_id'] ) ) {
    276                     $contact_id = $result['contact_id'];
    277                 }
    278             } else {
    279                 // Create new contact.
    280                 $this->logger->debug( 'Creating new Intercom contact', array( 'user_id' => $user_id ) );
    281                 $result = $this->contacts->create( $user_data );
    282                 // Get contact ID from result.
    283                 if ( is_array( $result ) && isset( $result['contact_id'] ) ) {
    284                     $contact_id = $result['contact_id'];
    285                 }
    286             }
    287 
    288             // Sync tags if enabled.
    289             if ( ! empty( $contact_id ) ) {
    290                 $tags = new RPPLSTP_IWS_Intercom_Tags();
    291                 $tags->sync_tags_for_contact( $contact_id, $user_id );
    292             }
    293 
    294             $this->logger->info( 'User sync completed successfully', array( 'user_id' => $user_id ) );
    295             return $result;
     286        }
     287
     288        $this->logger->info( 'User sync completed successfully', array( 'user_id' => $user_id ) );
     289        return $result;
    296290
    297291        } catch ( IntercomApiException $e ) {
  • sync-engine-for-intercom/trunk/sync-engine-for-intercom.php

    r3420647 r3450989  
    44 * Plugin URI: https://ripplestep.com/
    55 * Description: Sync WordPress and WooCommerce users, events, tags, and customer data with Intercom in real time.
    6  * Version: 1.0.3
     6 * Version: 2.0.1
    77 * Author: RippleStep
    88 * Author URI: https://ripplestep.com
     
    3131
    3232// Define plugin constants.
    33 define( 'RPPLSTP_IWS_VERSION', '1.0.0' );
     33define( 'RPPLSTP_IWS_VERSION', '2.0.1' );
    3434define( 'RPPLSTP_IWS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    3535define( 'RPPLSTP_IWS_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
     
    147147     */
    148148    private function init_hooks(): void {
     149        add_action( 'before_woocommerce_init', array( $this, 'declare_woocommerce_hpos_compatibility' ) );
    149150        add_action( 'admin_init', array( $this, 'check_requirements' ) );
    150151
     
    164165        // Initialize chat widget.
    165166        add_action( 'wp_footer', array( $this, 'output_chat_widget' ) );
     167    }
     168
     169    /**
     170     * Declare compatibility with WooCommerce HPOS (High-Performance Order Storage).
     171     *
     172     * Must be called from the before_woocommerce_init hook. Ensures the plugin
     173     * is not flagged as incompatible when HPOS (custom order tables) is enabled.
     174     *
     175     * @since 1.0.0
     176     * @return void
     177     */
     178    public function declare_woocommerce_hpos_compatibility(): void {
     179        if ( ! class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
     180            return;
     181        }
     182        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
    166183    }
    167184
Note: See TracChangeset for help on using the changeset viewer.