delete(); CampaignUrlMetric::whereIn('subscriber_id', $subscriberIds)->delete(); SubscriberMeta::whereIn('subscriber_id', $subscriberIds)->delete(); SubscriberNote::whereIn('subscriber_id', $subscriberIds)->delete(); SubscriberPivot::whereIn('subscriber_id', $subscriberIds)->delete(); FunnelMetric::whereIn('subscriber_id', $subscriberIds)->delete(); FunnelSubscriber::whereIn('subscriber_id', $subscriberIds)->delete(); if (defined('FLUENTCAMPAIGN_DIR_FILE')) { \FluentCampaign\App\Models\SequenceTracker::whereIn('subscriber_id', $subscriberIds)->delete(); } if (Helper::isExperimentalEnabled('company_module')) { Company::whereIn('owner_id', $subscriberIds) ->update([ 'owner_id' => NULL ]); } } /** * Cleanup related data of a campaign. * * @param int $campaignId */ public function deleteCampaignAssets($campaignId) { // Idempotent backstop — Campaign::deleteCampaignData() already removes // these in the normal delete flow, but we keep this here so any // future caller that fires fluent_crm/campaign_deleted without // running deleteCampaignData() first still gets a clean teardown. CampaignEmail::where('campaign_id', $campaignId)->delete(); CampaignUrlMetric::where('campaign_id', $campaignId)->delete(); } /** * Cleanup related data of a list. * * @param int $listId */ public function deleteListAssets($listId) { SubscriberPivot::where('object_type', 'FluentCrm\App\Models\Lists')->where('object_id', $listId)->delete(); } /** * Cleanup related data of a tag. * * @param int $listId */ public function deleteTagAssets($listId) { SubscriberPivot::where('object_type', 'FluentCrm\App\Models\Tag')->where('object_id', $listId)->delete(); } /** * Cancel Future Emails. * * @param \FluentCrm\App\Models\Subscriber $subscriber */ public function handleUnsubscribe($subscriber) { // Per-statement try/catch: a row-lock deadlock against the mailer // workers on the CampaignEmail update should not also block the // FunnelSubscriber / SequenceTracker cancellations. The next status // transition (or a manual retry) will reconcile any rows we miss. try { CampaignEmail::where('subscriber_id', $subscriber->id) ->whereIn('status', ['pending', 'scheduled', 'draft', 'processing', 'scheduling']) ->update([ 'status' => 'cancelled' ]); } catch (\Exception $e) { Helper::debugLog('handleUnsubscribe', 'CampaignEmail cancel deferred: ' . $e->getMessage(), 'extended'); } try { FunnelSubscriber::where('subscriber_id', $subscriber->id) ->where('status', 'active') ->whereDoesntHave('funnel', function ($query) { $query->where('trigger_name', 'fluent_crm/subscriber_status_changed'); }) ->update([ 'status' => 'cancelled' ]); } catch (\Exception $e) { Helper::debugLog('handleUnsubscribe', 'FunnelSubscriber cancel deferred: ' . $e->getMessage(), 'extended'); } if (defined('FLUENTCAMPAIGN')) { try { \FluentCampaign\App\Models\SequenceTracker::where('subscriber_id', $subscriber->id) ->where('status', 'active') ->update([ 'status' => 'cancelled' ]); } catch (\Exception $e) { Helper::debugLog('handleUnsubscribe', 'SequenceTracker cancel deferred: ' . $e->getMessage(), 'extended'); } } } /** * Change the future emails email_address of a provided contact. * * @param \FluentCrm\App\Models\Subscriber $subscriber */ public function handleContactEmailChanged($subscriber) { CampaignEmail::where('subscriber_id', $subscriber->id) ->whereIn('status', ['draft', 'scheduled']) ->update([ 'email_address' => $subscriber->email ]); } /** * @param $userId int * @param $resign int|null * @param $deletedUser \WP_User * @return bool */ public function handleUserDelete($userId, $resign, $deletedUser) { $settings = Helper::getComplianceSettings(); if ($settings['delete_contact_on_user'] !== 'yes') { return false; } $subscriber = Subscriber::where('user_id', $userId)->first(); if (!$subscriber && $deletedUser) { $subscriber = Subscriber::where('email', $deletedUser->user_email)->first(); } if (!$subscriber) { return false; } // delete the subscriber now; Helper::deleteContacts([$subscriber->id]); return true; } public function attachCrmExporter($exporters) { $settings = Helper::getComplianceSettings(); if ($settings['personal_data_export'] !== 'yes') { return $exporters; } $exporters['fluent-crm'] = [ 'exporter_friendly_name' => __('FluentCRM Data', 'fluent-crm'), 'callback' => [$this, 'exportPersonalDataWP'], ]; return $exporters; } public function exportPersonalDataWP($user_email, $page = 1) { $subscriber = Subscriber::where('email', $user_email)->first(); if (!$subscriber) { return [ 'data' => [], 'done' => true ]; } $customerFields = $subscriber->custom_fields(); $mainFields = $subscriber->toArray(); $data = [ 'group_id' => 'fluent-crm-contact', 'group_label' => __('FluentCRM Data', 'fluent-crm'), 'item_id' => 'crm-contact', 'data' => [] ]; foreach ($mainFields as $fieldKey => $fieldValue) { if ($fieldValue) { $data['data'][] = [ 'name' => $fieldKey, 'value' => $fieldValue ]; } } foreach ($customerFields as $fieldKey => $customerField) { $data['data'][] = [ 'name' => $fieldKey, 'value' => $customerField ]; } return [ 'data' => [$data], 'done' => true, ]; } public function handleCompanyDelete($id) { /* * Remove Company ID from all connected subscribers */ Subscriber::where('company_id', $id)->update([ 'company_id' => NULL ]); fluentCrmDb()->table('fc_subscriber_pivot') ->where('object_id', $id) ->where('object_type', 'FluentCrm\App\Models\Company') ->delete(); // Delete company notes CompanyNote::where('subscriber_id', $id)->delete(); } public function handleUserPasswordChanged($user) { $contact = Subscriber::where('email', $user->user_email) ->first(); if (!$contact) { return false; } $exist = SubscriberMeta::where('subscriber_id', $contact->id) ->where('key', '_secure_managed_hash') ->first(); if (!$exist) { return false; } $hash = md5(wp_generate_uuid4() . '_' . $contact->id . '_' . '_' . time() . '__' . $contact->id); $exist->value = $hash; $exist->updated_at = current_time('mysql'); $exist->save(); return true; } public function archiveCampaignAssets($campaign) { if ($campaign->type != 'campaign' || fluentcrm_get_campaign_meta($campaign->id, '_cached_email_body', true)) { return; } // We will create email body and then cache it for future use $rawTemplates = [ 'raw_html', 'visual_builder', 'raw_classic' ]; if (in_array($campaign->design_template, $rawTemplates)) { $emailBody = $campaign->email_body; } else { $emailBody = (new BlockParser())->parse($campaign->email_body); } fluentcrm_update_campaign_meta($campaign->id, '_cached_email_body', $emailBody); return true; } public static function maybeRemoveOldScheuledActionLogs() { $group_slug = 'fluent-crm'; $days_old = 7; global $wpdb; // Get the timestamp for 7 days ago $cutoff_date = gmdate('Y-m-d H:i:s', strtotime("-{$days_old} days")); // Get the group ID $group_id = $wpdb->get_var($wpdb->prepare( "SELECT group_id FROM {$wpdb->prefix}actionscheduler_groups WHERE slug = %s", $group_slug )); if (!$group_id) { return false; // Group not found } // Delete old actions and their associated logs $deleted = $wpdb->query($wpdb->prepare(" DELETE a, l FROM {$wpdb->prefix}actionscheduler_actions a LEFT JOIN {$wpdb->prefix}actionscheduler_logs l ON a.action_id = l.action_id WHERE a.group_id = %d AND a.status IN ('complete', 'failed') AND a.scheduled_date_gmt < %s", $group_id, $cutoff_date)); // Clean up orphaned claims $wpdb->query(" DELETE c FROM {$wpdb->prefix}actionscheduler_claims c LEFT JOIN {$wpdb->prefix}actionscheduler_actions a ON c.claim_id = a.claim_id WHERE a.action_id IS NULL"); return $deleted; } public function SyncSubscriberDeleteSettings($fromKey, $value) { if ($fromKey == 'compliance_settings') { $option = Meta::where('key', 'user_syncing_settings') ->where('object_type', 'option') ->first(); if ($option) { $settings = $option->value; if ($settings['delete_contact_on_user_delete'] != $value) { $settings['delete_contact_on_user_delete'] = $value; $option->value = $settings; $option->save(); } } } else { $complianceSettings = get_option('_fluentcrm_compliance_settings'); if ($complianceSettings) { $complianceSettings['delete_contact_on_user'] = $value; update_option('_fluentcrm_compliance_settings', $complianceSettings, 'no'); } } } }