Initial commit
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel\Actions;
|
||||
|
||||
use FluentCrm\App\Models\Subscriber;
|
||||
use FluentCrm\App\Services\Funnel\BaseAction;
|
||||
use FluentCrm\App\Services\Funnel\FunnelHelper;
|
||||
|
||||
class ApplyCompanyAction extends BaseAction
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->actionName = 'add_contact_to_company';
|
||||
$this->priority = 20;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getBlock()
|
||||
{
|
||||
return [
|
||||
'category' => __('CRM', 'fluent-crm'),
|
||||
'title' => __('Apply Company', 'fluent-crm'),
|
||||
'description' => __('Add contact to the selected company', 'fluent-crm'),
|
||||
'icon' => 'fc-icon-apply_list',//fluentCrmMix('images/funnel_icons/apply_list.svg'),
|
||||
'settings' => [
|
||||
'company' => null
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getBlockFields()
|
||||
{
|
||||
return [
|
||||
'title' => __('Apply Company to the contact', 'fluent-crm'),
|
||||
'sub_title' => __('Select which list will be added to the contact', 'fluent-crm'),
|
||||
'fields' => [
|
||||
'company' => [
|
||||
'type' => 'option_selectors',
|
||||
'option_key' => 'companies',
|
||||
'label' => __('Select Company', 'fluent-crm'),
|
||||
'placeholder' => __('Select Company', 'fluent-crm')
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function handle($subscriber, $sequence, $funnelSubscriberId, $funnelMetric)
|
||||
{
|
||||
if (empty($sequence->settings['company'])) {
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
$company = $sequence->settings['company'];
|
||||
|
||||
$renewedSubscriber = Subscriber::where('id', $subscriber->id)->first();
|
||||
if (!$renewedSubscriber) {
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
|
||||
return;
|
||||
}
|
||||
$renewedSubscriber->attachCompanies([$company]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel\Actions;
|
||||
|
||||
use FluentCrm\App\Models\Subscriber;
|
||||
use FluentCrm\App\Services\Funnel\BaseAction;
|
||||
use FluentCrm\App\Services\Funnel\FunnelHelper;
|
||||
|
||||
class ApplyListAction extends BaseAction
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->actionName = 'add_contact_to_list';
|
||||
$this->priority = 20;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getBlock()
|
||||
{
|
||||
return [
|
||||
'category' => __('CRM', 'fluent-crm'),
|
||||
'title' => __('Apply List', 'fluent-crm'),
|
||||
'description' => __('Add contact to the selected lists', 'fluent-crm'),
|
||||
'icon' => 'fc-icon-apply_list',//fluentCrmMix('images/funnel_icons/apply_list.svg'),
|
||||
'settings' => [
|
||||
'lists' => []
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getBlockFields()
|
||||
{
|
||||
return [
|
||||
'title' => __('Apply List to the contact', 'fluent-crm'),
|
||||
'sub_title' => __('Select which list will be added to the contact', 'fluent-crm'),
|
||||
'fields' => [
|
||||
'lists' => [
|
||||
'type' => 'option_selectors',
|
||||
'option_key' => 'lists',
|
||||
'is_multiple' => true,
|
||||
'label' => __('Select Lists', 'fluent-crm'),
|
||||
'placeholder' => __('Select List', 'fluent-crm'),
|
||||
'creatable' => true
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function handle($subscriber, $sequence, $funnelSubscriberId, $funnelMetric)
|
||||
{
|
||||
if (empty($sequence->settings['lists']) || !is_array($sequence->settings['lists'])) {
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
$lists = $sequence->settings['lists'];
|
||||
|
||||
$renewedSubscriber = Subscriber::where('id', $subscriber->id)->first();
|
||||
if (!$renewedSubscriber) {
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
|
||||
return;
|
||||
}
|
||||
$renewedSubscriber->attachLists($lists);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel\Actions;
|
||||
|
||||
use FluentCrm\App\Models\Subscriber;
|
||||
use FluentCrm\App\Services\Funnel\BaseAction;
|
||||
use FluentCrm\App\Services\Funnel\FunnelHelper;
|
||||
|
||||
class ApplyTagAction extends BaseAction
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->actionName = 'add_contact_to_tag';
|
||||
$this->priority = 21;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getBlock()
|
||||
{
|
||||
return [
|
||||
'category' => __('CRM', 'fluent-crm'),
|
||||
'title' => __('Apply Tag', 'fluent-crm'),
|
||||
'description' => __('Add this contact to the selected Tags', 'fluent-crm'),
|
||||
'icon' => 'fc-icon-apply_tag',//fluentCrmMix('images/funnel_icons/apply_tag.svg'),
|
||||
'settings' => [
|
||||
'tags' => []
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getBlockFields()
|
||||
{
|
||||
return [
|
||||
'title' => __('Apply Tag to the contact', 'fluent-crm'),
|
||||
'sub_title' => __('Select which tag will be added to the contact', 'fluent-crm'),
|
||||
'fields' => [
|
||||
'tags' => [
|
||||
'type' => 'option_selectors',
|
||||
'option_key' => 'tags',
|
||||
'is_multiple' => true,
|
||||
'creatable' => true,
|
||||
'label' => __('Select Tags', 'fluent-crm'),
|
||||
'placeholder' => __('Select Tag', 'fluent-crm')
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function handle($subscriber, $sequence, $funnelSubscriberId, $funnelMetric)
|
||||
{
|
||||
if (empty($sequence->settings['tags']) || !is_array($sequence->settings['tags'])) {
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
$tags = $sequence->settings['tags'];
|
||||
$renewedSubscriber = Subscriber::where('id', $subscriber->id)->first();
|
||||
if (!$renewedSubscriber) {
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
|
||||
return;
|
||||
}
|
||||
$renewedSubscriber->attachTags($tags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel\Actions;
|
||||
|
||||
use FluentCrm\App\Models\Subscriber;
|
||||
use FluentCrm\App\Services\Funnel\BaseAction;
|
||||
use FluentCrm\App\Services\Funnel\FunnelHelper;
|
||||
|
||||
class DetachCompanyAction extends BaseAction
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->actionName = 'detach_contact_from_company';
|
||||
$this->priority = 22;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getBlock()
|
||||
{
|
||||
return [
|
||||
'category' => __('CRM', 'fluent-crm'),
|
||||
'title' => __('Remove From Company', 'fluent-crm'),
|
||||
'description' => __('Remove this contact from the selected company', 'fluent-crm'),
|
||||
'icon' => 'fc-icon-removed_list',//fluentCrmMix('images/funnel_icons/list_remove.svg'),
|
||||
'settings' => [
|
||||
'company' => null
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getBlockFields()
|
||||
{
|
||||
return [
|
||||
'title' => __('Remove Contact from the Selected Company', 'fluent-crm'),
|
||||
'sub_title' => __('Select Company that you want to remove from targeted Contact', 'fluent-crm'),
|
||||
'fields' => [
|
||||
'company' => [
|
||||
'type' => 'option_selectors',
|
||||
'option_key' => 'companies',
|
||||
'label' => __('Select Company', 'fluent-crm'),
|
||||
'placeholder' => __('Select Company', 'fluent-crm'),
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function handle($subscriber, $sequence, $funnelSubscriberId, $funnelMetric)
|
||||
{
|
||||
if (empty($sequence->settings['company'])) {
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
$company = $sequence->settings['company'];
|
||||
|
||||
$renewedSubscriber = Subscriber::where('id', $subscriber->id)->first();
|
||||
if (!$renewedSubscriber) {
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
|
||||
return;
|
||||
}
|
||||
$renewedSubscriber->detachCompanies([$company]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel\Actions;
|
||||
|
||||
use FluentCrm\App\Models\Subscriber;
|
||||
use FluentCrm\App\Services\Funnel\BaseAction;
|
||||
use FluentCrm\App\Services\Funnel\FunnelHelper;
|
||||
|
||||
class DetachListAction extends BaseAction
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->actionName = 'detach_contact_from_list';
|
||||
$this->priority = 22;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getBlock()
|
||||
{
|
||||
return [
|
||||
'category' => __('CRM', 'fluent-crm'),
|
||||
'title' => __('Remove From List', 'fluent-crm'),
|
||||
'description' => __('Remove this contact from the selected lists', 'fluent-crm'),
|
||||
'icon' => 'fc-icon-removed_list',//fluentCrmMix('images/funnel_icons/list_remove.svg'),
|
||||
'settings' => [
|
||||
'lists' => []
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getBlockFields()
|
||||
{
|
||||
return [
|
||||
'title' => __('Remove Contact from the Selected Lists', 'fluent-crm'),
|
||||
'sub_title' => __('Select Lists that you want to remove from targeted Contact', 'fluent-crm'),
|
||||
'fields' => [
|
||||
'lists' => [
|
||||
'type' => 'option_selectors',
|
||||
'option_key' => 'lists',
|
||||
'is_multiple' => true,
|
||||
'label' => __('Select Lists', 'fluent-crm'),
|
||||
'placeholder' => __('Select List', 'fluent-crm'),
|
||||
'creatable' => true
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function handle($subscriber, $sequence, $funnelSubscriberId, $funnelMetric)
|
||||
{
|
||||
if (empty($sequence->settings['lists']) || !is_array($sequence->settings['lists'])) {
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
$lists = $sequence->settings['lists'];
|
||||
|
||||
$renewedSubscriber = Subscriber::where('id', $subscriber->id)->first();
|
||||
if (!$renewedSubscriber) {
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
|
||||
return;
|
||||
}
|
||||
$renewedSubscriber->detachLists($lists);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel\Actions;
|
||||
|
||||
use FluentCrm\App\Models\Subscriber;
|
||||
use FluentCrm\App\Services\Funnel\BaseAction;
|
||||
use FluentCrm\App\Services\Funnel\FunnelHelper;
|
||||
|
||||
class DetachTagAction extends BaseAction
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->actionName = 'detach_contact_from_tag';
|
||||
$this->priority = 23;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getBlock()
|
||||
{
|
||||
return [
|
||||
'category' => __('CRM', 'fluent-crm'),
|
||||
'title' => __('Remove From Tag', 'fluent-crm'),
|
||||
'description' => __('Remove this contact from the selected Tags', 'fluent-crm'),
|
||||
'icon' => 'fc-icon-tag_removed',//fluentCrmMix('images/funnel_icons/tag_remove.svg'),
|
||||
'settings' => [
|
||||
'tags' => []
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getBlockFields()
|
||||
{
|
||||
return [
|
||||
'title' => __('Remove Contact from the Selected Tags', 'fluent-crm'),
|
||||
'sub_title' => __('Select Tags that you want to remove from targeted Contact', 'fluent-crm'),
|
||||
'fields' => [
|
||||
'tags' => [
|
||||
'type' => 'option_selectors',
|
||||
'option_key' => 'tags',
|
||||
'is_multiple' => true,
|
||||
'label' => __('Select Tags', 'fluent-crm'),
|
||||
'placeholder' => __('Select Tag', 'fluent-crm'),
|
||||
'creatable' => true
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function handle($subscriber, $sequence, $funnelSubscriberId, $funnelMetric)
|
||||
{
|
||||
if (empty($sequence->settings['tags']) || !is_array($sequence->settings['tags'])) {
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
$tags = $sequence->settings['tags'];
|
||||
|
||||
$renewedSubscriber = Subscriber::where('id', $subscriber->id)->first();
|
||||
if (!$renewedSubscriber) {
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
|
||||
return;
|
||||
}
|
||||
$renewedSubscriber->detachTags($tags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel\Actions;
|
||||
|
||||
use FluentCrm\App\Models\CampaignEmail;
|
||||
use FluentCrm\App\Models\CampaignUrlMetric;
|
||||
use FluentCrm\App\Models\FunnelCampaign;
|
||||
use FluentCrm\App\Services\Funnel\BaseAction;
|
||||
use FluentCrm\App\Services\Funnel\FunnelHelper;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
class SendEmailAction extends BaseAction
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->actionName = 'send_custom_email';
|
||||
$this->priority = 10;
|
||||
parent::__construct();
|
||||
add_filter('fluentcrm_funnel_sequence_saving_' . $this->actionName, array($this, 'savingAction'), 10, 2);
|
||||
add_filter('fluentcrm_funnel_sequence_deleting_' . $this->actionName, array($this, 'deleteAction'), 10, 2);
|
||||
add_filter('fluentcrm_funnel_sequence_filtered_' . $this->actionName, array($this, 'gettingAction'), 10, 2);
|
||||
}
|
||||
|
||||
public function getBlock()
|
||||
{
|
||||
return [
|
||||
'category' => __('Email', 'fluent-crm'),
|
||||
'title' => __('Send Custom Email', 'fluent-crm'),
|
||||
'description' => __('Send a custom Email to your subscriber or custom email address', 'fluent-crm'),
|
||||
'icon' => 'fc-icon-send_campaign',//fluentCrmMix('images/funnel_icons/custom_email.svg'),
|
||||
'settings' => [
|
||||
'reference_campaign' => '',
|
||||
'send_email_to_type' => 'contact',
|
||||
'send_email_custom' => '',
|
||||
'campaign' => FunnelCampaign::getMock(),
|
||||
'is_scheduled' => 'no',
|
||||
'scheduled_at' => '',
|
||||
'skip_if_overdue' => 'no',
|
||||
'mailer_settings' => [
|
||||
'from_name' => '',
|
||||
'from_email' => '',
|
||||
'reply_to_name' => '',
|
||||
'reply_to_email' => '',
|
||||
'is_custom' => 'no'
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getBlockFields()
|
||||
{
|
||||
return [
|
||||
'title' => __('Send Custom Email', 'fluent-crm'),
|
||||
'sub_title' => __('Please provide email details that you want to send', 'fluent-crm'),
|
||||
'fields' => [
|
||||
'send_email_to_type' => [
|
||||
'type' => 'radio',
|
||||
'wrapper_class' => 'fc_half_field',
|
||||
'label' => __('Send Email to', 'fluent-crm'),
|
||||
'options' => [
|
||||
[
|
||||
'id' => 'contact',
|
||||
'title' => __('Send To the contact', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'custom',
|
||||
'title' => __('Send to Custom Email Address', 'fluent-crm')
|
||||
]
|
||||
]
|
||||
],
|
||||
'send_email_custom' => [
|
||||
'wrapper_class' => 'fcrm_send_custom_email_form_wrap',
|
||||
'type' => 'input-text',
|
||||
'label' => __('Send To Email Addresses (If Custom)', 'fluent-crm'),
|
||||
'placeholder' => __('Custom Email Addresses', 'fluent-crm'),
|
||||
'inline_help' => __('Use comma separated values for multiple', 'fluent-crm'),
|
||||
'dependency' => [
|
||||
'depends_on' => 'send_email_to_type',
|
||||
'operator' => '=',
|
||||
'value' => 'custom'
|
||||
]
|
||||
],
|
||||
'campaign' => [
|
||||
'label' => '',
|
||||
'wrapper_class' => 'fc_email_writer',
|
||||
'type' => 'email_campaign_composer'
|
||||
],
|
||||
'is_scheduled' => [
|
||||
'type' => 'yes_no_check',
|
||||
'check_label' => __('Schedule this email to a specific date', 'fluent-crm'),
|
||||
'wrapper_class' => 'fc_no_pad_l',
|
||||
],
|
||||
'skip_if_overdue' => [
|
||||
'type' => 'yes_no_check',
|
||||
'wrapper_class' => 'fcrm_child_field',
|
||||
'check_label' => __('Skip sending email if date is overdued', 'fluent-crm'),
|
||||
'dependency' => [
|
||||
'depends_on' => 'is_scheduled',
|
||||
'operator' => '=',
|
||||
'value' => 'yes'
|
||||
]
|
||||
],
|
||||
'scheduled_at' => [
|
||||
'label' => __('Schedule Date and Time', 'fluent-crm'),
|
||||
'type' => 'date_time',
|
||||
'wrapper_class' => 'fcrm_child_field fcrm_schedule_date_time fc_no_marg_b',
|
||||
'placeholder' => __('Select Date and Time', 'fluent-crm'),
|
||||
'inline_help' => __('If schedule date is past in the runtime then email will be sent immediately', 'fluent-crm'),
|
||||
'dependency' => [
|
||||
'depends_on' => 'is_scheduled',
|
||||
'operator' => '=',
|
||||
'value' => 'yes'
|
||||
]
|
||||
],
|
||||
'mailer_settings' => [
|
||||
'type' => 'custom_sender_config',
|
||||
'check_label' => __('Set Custom From Name and Email', 'fluent-crm')
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function savingAction($sequence, $funnel)
|
||||
{
|
||||
$funnelCampaign = Arr::get($sequence, 'settings.campaign', []);
|
||||
$funnelCampaignId = Arr::get($funnelCampaign, 'id');
|
||||
|
||||
$isStripped = Arr::get($sequence, 'is_stripped');
|
||||
if ($isStripped && $funnelCampaignId) {
|
||||
$refCampaign = FunnelCampaign::find($funnelCampaignId);
|
||||
if (!$refCampaign) {
|
||||
return $sequence;
|
||||
}
|
||||
$data = Arr::only($refCampaign->toArray(), array_keys(FunnelCampaign::getMock()));
|
||||
} else {
|
||||
$data = Arr::only($funnelCampaign, array_keys(FunnelCampaign::getMock()));
|
||||
}
|
||||
|
||||
$sequenceId = Arr::get($sequence, 'id');
|
||||
$data['settings']['mailer_settings'] = Arr::get($sequence, 'settings.mailer_settings', []);
|
||||
|
||||
if ($funnelCampaignId && $funnel->id == Arr::get($data, 'parent_id')) {
|
||||
// We have this campaign
|
||||
$data['settings'] = \maybe_serialize($data['settings']);
|
||||
$data['type'] = 'funnel_email_campaign';
|
||||
$data['title'] = $funnel->title . ' (' . $funnel->id . ' - ' . $sequenceId . ')';
|
||||
FunnelCampaign::where('id', $funnelCampaignId)->update($data);
|
||||
} else {
|
||||
$data['parent_id'] = $funnel->id;
|
||||
$data['type'] = 'funnel_email_campaign';
|
||||
$data['title'] = $funnel->title . ' (' . $funnel->id . ' - ' . $sequenceId . ')';
|
||||
$campaign = FunnelCampaign::create($data);
|
||||
$funnelCampaignId = $campaign->id;
|
||||
|
||||
if ($data['design_template'] == 'visual_builder' && !empty($funnelCampaign['_visual_builder_design'])) {
|
||||
fluentcrm_update_campaign_meta($funnelCampaignId, '_visual_builder_design', $funnelCampaign['_visual_builder_design']);
|
||||
}
|
||||
}
|
||||
|
||||
$sequence['settings']['reference_campaign'] = $funnelCampaignId;
|
||||
$sequence['settings']['campaign'] = [];
|
||||
return $sequence;
|
||||
}
|
||||
|
||||
public function gettingAction($sequence, $funnel)
|
||||
{
|
||||
$refCampaignData = FunnelCampaign::getMock();
|
||||
if ($refCampaignId = Arr::get($sequence, 'settings.reference_campaign')) {
|
||||
$refCampaign = FunnelCampaign::find($refCampaignId);
|
||||
if ($refCampaign) {
|
||||
$refCampaignData = Arr::only($refCampaign->toArray(), array_keys(FunnelCampaign::getMock()));
|
||||
|
||||
if ($refCampaignData['design_template'] == 'visual_builder') {
|
||||
$refCampaignData['_visual_builder_design'] = fluentcrm_get_campaign_meta($refCampaignData['id'], '_visual_builder_design', true);
|
||||
} else {
|
||||
$refCampaignData['__fcrm_block_type'] = 'email_body_in_funnel';
|
||||
}
|
||||
$sequence['settings']['campaign'] = $refCampaignData;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($sequence['settings']['mailer_settings'])) {
|
||||
$sequence['settings']['mailer_settings'] = [
|
||||
'from_name' => '',
|
||||
'from_email' => '',
|
||||
'reply_to_name' => '',
|
||||
'reply_to_email' => '',
|
||||
'is_custom' => 'no'
|
||||
];
|
||||
}
|
||||
|
||||
return $sequence;
|
||||
}
|
||||
|
||||
public function deleteAction($sequence, $funnel)
|
||||
{
|
||||
$refCampaign = Arr::get($sequence->settings, 'reference_campaign');
|
||||
if ($refCampaign) {
|
||||
FunnelCampaign::where('id', $refCampaign)->delete();
|
||||
CampaignEmail::where('campaign_id', $refCampaign)->delete();
|
||||
CampaignUrlMetric::where('campaign_id', $refCampaign)->delete();
|
||||
fluentcrm_delete_campaign_meta($refCampaign, '');
|
||||
}
|
||||
}
|
||||
|
||||
public function handle($subscriber, $sequence, $funnelSubscriberId, $funnelMetric)
|
||||
{
|
||||
$settings = $sequence->settings;
|
||||
$refCampaign = Arr::get($settings, 'reference_campaign');
|
||||
|
||||
if (!$refCampaign) {
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($funnelMetric) {
|
||||
// We are making sure, this action will run only once per funnel subscriber
|
||||
if (did_action('fluent_crm/did_run_funnel_email_action_' . $funnelMetric->id)) {
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
do_action('fluent_crm/did_run_funnel_email_action_' . $funnelMetric->id);
|
||||
}
|
||||
|
||||
$campaign = FunnelCampaign::find($refCampaign);
|
||||
if (!$campaign) {
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
|
||||
return;
|
||||
}
|
||||
|
||||
$scheduledAt = current_time('mysql');
|
||||
if (Arr::get($settings, 'is_scheduled') == 'yes') {
|
||||
$providedDate = Arr::get($settings, 'scheduled_at');
|
||||
|
||||
if ($providedDate && Arr::get($settings, 'skip_if_overdue') == 'yes') {
|
||||
if (strtotime($scheduledAt) > strtotime($providedDate)) {
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'skipped');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($providedDate && strtotime($providedDate) > strtotime($scheduledAt)) {
|
||||
$scheduledAt = $providedDate;
|
||||
}
|
||||
}
|
||||
|
||||
$args = [
|
||||
'status' => 'scheduled',
|
||||
'scheduled_at' => $scheduledAt,
|
||||
'email_type' => 'funnel_email_campaign',
|
||||
'note' => __('Email Sent From Funnel: ', 'fluent-crm') . $campaign->title
|
||||
];
|
||||
|
||||
if (Arr::get($settings, 'send_email_to_type') == 'contact') {
|
||||
$campaign->processAndSubscribe($subscriber, [
|
||||
'funnel_subscriber_id' => $funnelSubscriberId
|
||||
], $args);
|
||||
|
||||
do_action('fluentcrm_process_contact_jobs', $subscriber);
|
||||
} else if ($customAddresses = Arr::get($settings, 'send_email_custom')) {
|
||||
/**
|
||||
* Filters and parses custom email addresses for a campaign email text.
|
||||
*
|
||||
* This code applies the 'fluent_crm/parse_campaign_email_text' filter to process
|
||||
* the custom email addresses for a given subscriber.
|
||||
*
|
||||
* @param string $customAddresses The custom email addresses string to be parsed.
|
||||
* @param object $subscriber The subscriber object containing subscriber details.
|
||||
*
|
||||
* @return string The filtered and parsed custom email addresses string.
|
||||
*/
|
||||
|
||||
$customAddresses = apply_filters('fluent_crm/parse_campaign_email_text', $customAddresses, $subscriber);
|
||||
$customAddresses = array_filter(array_map('sanitize_email', explode(',', $customAddresses)));
|
||||
$campaign->sendToCustomAddresses($customAddresses, $args, $subscriber);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel\Actions;
|
||||
|
||||
use FluentCrm\App\Services\Funnel\BaseAction;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
class WaitTimeAction extends BaseAction
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->actionName = 'fluentcrm_wait_times';
|
||||
$this->priority = 10;
|
||||
parent::__construct();
|
||||
|
||||
add_filter('fluentcrm_funnel_sequence_filtered_' . $this->actionName, array($this, 'gettingAction'), 10, 2);
|
||||
}
|
||||
|
||||
public function getBlock()
|
||||
{
|
||||
return [
|
||||
'category' => __('CRM', 'fluent-crm'),
|
||||
'title' => __('Wait X Days/Hours', 'fluent-crm'),
|
||||
'description' => __('Wait defined timespan before execute the next action', 'fluent-crm'),
|
||||
'icon' => 'fc-icon-wait_time',
|
||||
'settings' => [
|
||||
'wait_type' => 'unit_wait',
|
||||
'wait_time_amount' => '',
|
||||
'wait_time_unit' => 'days',
|
||||
'is_timestamp_wait' => '',
|
||||
'wait_date_time' => '',
|
||||
'to_day' => [],
|
||||
'to_day_time' => ''
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function gettingAction($sequence, $funnel)
|
||||
{
|
||||
if (empty($sequence['settings']['wait_type'])) {
|
||||
if (Arr::get($sequence, 'settings.is_timestamp_wait') == 'yes') {
|
||||
$sequence['settings']['wait_type'] = 'timestamp_wait';
|
||||
} else {
|
||||
$sequence['settings']['wait_type'] = 'unit_wait';
|
||||
}
|
||||
|
||||
$sequence['settings']['to_day'] = [];
|
||||
$sequence['settings']['to_day_time'] = '';
|
||||
}
|
||||
|
||||
if (!empty($sequence['settings']['to_day'])) {
|
||||
$sequence['settings']['to_day'] = array_map(function ($day) {
|
||||
return substr($day, 0, 3);
|
||||
}, $sequence['settings']['to_day']);
|
||||
}
|
||||
|
||||
return $sequence;
|
||||
}
|
||||
|
||||
public function getBlockFields()
|
||||
{
|
||||
|
||||
$customFields = FluentCrmApi('contacts')->getCustomFields(['date_time', 'date'], true);
|
||||
|
||||
// add date of birth field at the beginning
|
||||
array_unshift($customFields, [
|
||||
'id' => '__date_of_birth__',
|
||||
'title' => __('Contact\'s Next Date of Birth', 'fluent-crm')
|
||||
]);
|
||||
|
||||
return [
|
||||
'title' => __('Wait X Days/Hours', 'fluent-crm'),
|
||||
'sub_title' => __('Wait defined timespan before execute the next action', 'fluent-crm'),
|
||||
'fields' => [
|
||||
'wait_type' => [
|
||||
'type' => 'radio_buttons',
|
||||
'label' => __('Waiting Type', 'fluent-crm'),
|
||||
'options' => [
|
||||
[
|
||||
'id' => 'unit_wait',
|
||||
'title' => __('Wait by period', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'timestamp_wait',
|
||||
'title' => __('Wait Until Date', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'to_day',
|
||||
'title' => __('Wait by Weekday', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'by_custom_field',
|
||||
'title' => __('Wait by Custom Field', 'fluent-crm')
|
||||
]
|
||||
]
|
||||
],
|
||||
'wait_time_amount' => [
|
||||
'label' => __('Wait Time', 'fluent-crm'),
|
||||
'type' => 'input-number',
|
||||
'wrapper_class' => 'fc_2col_inline pad-r-20',
|
||||
'dependency' => [
|
||||
'depends_on' => 'wait_type',
|
||||
'value' => 'unit_wait',
|
||||
'operator' => '=',
|
||||
],
|
||||
],
|
||||
'wait_time_unit' => [
|
||||
'label' => __('Wait Time Unit', 'fluent-crm'),
|
||||
'type' => 'select',
|
||||
'wrapper_class' => 'fc_2col_inline',
|
||||
'options' => [
|
||||
[
|
||||
'id' => 'months',
|
||||
'title' => __('Months', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'days',
|
||||
'title' => __('Days', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'hours',
|
||||
'title' => __('Hours', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'minutes',
|
||||
'title' => __('Minutes', 'fluent-crm')
|
||||
]
|
||||
],
|
||||
'dependency' => [
|
||||
'depends_on' => 'wait_type',
|
||||
'value' => 'unit_wait',
|
||||
'operator' => '=',
|
||||
],
|
||||
],
|
||||
'wait_date_time' => [
|
||||
'label' => __('Specify Date and Time', 'fluent-crm'),
|
||||
'type' => 'date_time',
|
||||
'placeholder' => __('Select Date & Time', 'fluent-crm'),
|
||||
'inline_help' => __('Please input date and time and this step will be executed after that time (TimeZone will be as per your WordPress Date Time Zone)', 'fluent-crm'),
|
||||
'dependency' => [
|
||||
'depends_on' => 'wait_type',
|
||||
'value' => 'timestamp_wait',
|
||||
'operator' => '=',
|
||||
]
|
||||
],
|
||||
'to_day' => [
|
||||
'type' => 'checkboxes',
|
||||
'label' => __('Wait until next day(s) of the week', 'fluent-crm'),
|
||||
'wrapper_class' => 'fc_2col_inline pad-r-20',
|
||||
'options' => [
|
||||
[
|
||||
'id' => 'Mon',
|
||||
'title' => __('Mon', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'Tue',
|
||||
'title' => __('Tue', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'Wed',
|
||||
'title' => __('Wed', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'Thu',
|
||||
'title' => __('Thu', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'Fri',
|
||||
'title' => __('Fri', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'Sat',
|
||||
'title' => __('Sat', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'Sun',
|
||||
'title' => __('Sun', 'fluent-crm')
|
||||
]
|
||||
],
|
||||
'dependency' => [
|
||||
'depends_on' => 'wait_type',
|
||||
'value' => 'to_day',
|
||||
'operator' => '=',
|
||||
]
|
||||
],
|
||||
'to_day_time' => [
|
||||
'label' => __('Time of the day', 'fluent-crm'),
|
||||
'type' => 'time_selector',
|
||||
'placeholder' => __('Select Time', 'fluent-crm'),
|
||||
'wrapper_class' => 'fc_2col_inline',
|
||||
'picker_options' => [
|
||||
'start' => '00:00',
|
||||
'step' => '00:10',
|
||||
'end' => '23:59'
|
||||
],
|
||||
'dependency' => [
|
||||
'depends_on' => 'wait_type',
|
||||
'value' => 'to_day',
|
||||
'operator' => '=',
|
||||
]
|
||||
],
|
||||
'by_custom_field' => [
|
||||
'label' => __('Select Contact\'s Custom Field', 'fluent-crm'),
|
||||
'type' => 'select',
|
||||
'inline_help' => __('If no value is found in the contact\'s custom field or past date then it will wait only 1 minute by default', 'fluent-crm'),
|
||||
'options' => $customFields,
|
||||
'dependency' => [
|
||||
'depends_on' => 'wait_type',
|
||||
'value' => 'by_custom_field',
|
||||
'operator' => '=',
|
||||
]
|
||||
],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function handle($subscriber, $sequence, $funnelSubscriberId, $funnelMetric)
|
||||
{
|
||||
// No-op: delay is handled by the processor via sequence delay settings
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel;
|
||||
|
||||
abstract class BaseAction
|
||||
{
|
||||
protected $actionName;
|
||||
|
||||
protected $funnel;
|
||||
protected $priority = 10;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->register();
|
||||
}
|
||||
|
||||
public function register()
|
||||
{
|
||||
add_filter('fluentcrm_funnel_blocks', array($this, 'pushBlock'), $this->priority, 2);
|
||||
add_filter('fluentcrm_funnel_block_fields', array($this, 'pushBlockFields'), $this->priority, 2);
|
||||
add_action('fluentcrm_funnel_sequence_handle_' . $this->actionName, array($this, 'handle'), 10, 4);
|
||||
}
|
||||
|
||||
public function pushBlock($blocks, $funnel)
|
||||
{
|
||||
$this->funnel = $funnel;
|
||||
|
||||
$block = $this->getBlock();
|
||||
if($block) {
|
||||
$block['type'] = 'action';
|
||||
$blocks[$this->actionName] = $block;
|
||||
}
|
||||
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
public function pushBlockFields($fields, $funnel)
|
||||
{
|
||||
$this->funnel = $funnel;
|
||||
|
||||
$fields[$this->actionName] = $this->getBlockFields();
|
||||
return $fields;
|
||||
}
|
||||
|
||||
abstract public function getBlock();
|
||||
|
||||
abstract public function getBlockFields();
|
||||
|
||||
abstract public function handle($subscriber, $sequence, $funnelSubscriberId, $funnelMetric);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel;
|
||||
|
||||
abstract class BaseBenchMark
|
||||
{
|
||||
protected $triggerName;
|
||||
|
||||
protected $actionArgNum = 1;
|
||||
|
||||
protected $priority = 10;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->register();
|
||||
}
|
||||
|
||||
public function register()
|
||||
{
|
||||
add_filter('fluentcrm_funnel_blocks', array($this, 'addBenchmark'), $this->priority, 1);
|
||||
|
||||
add_filter('fluentcrm_funnel_block_fields', array($this, 'pushBlockFields'), $this->priority, 2);
|
||||
|
||||
add_action('fluentcrm_funnel_benchmark_start_' . $this->triggerName, array($this, 'handle'), $this->priority, 2);
|
||||
|
||||
|
||||
add_filter('fluent_crm/benchmark_already_asserted_'.$this->triggerName, [$this, 'assertCurrentGoalState'], 10, 3);
|
||||
|
||||
add_filter('fluentcrm_funnel_arg_num_' . $this->triggerName, function ($num) {
|
||||
if ($num >= $this->actionArgNum) {
|
||||
return $num;
|
||||
}
|
||||
return $this->actionArgNum;
|
||||
});
|
||||
|
||||
/**
|
||||
* Filter the funnel sequence before saving.
|
||||
*
|
||||
* The dynamic portion of the hook name, `$this->triggerName`, refers to the name of the trigger.
|
||||
*
|
||||
* @since 2.6.0
|
||||
*
|
||||
* @param array $sequence The sequence data to be filtered.
|
||||
*
|
||||
* @return array The filtered sequence data.
|
||||
*/
|
||||
add_filter('fluentcrm_funnel_sequence_saving_' . $this->triggerName, function ($sequence) {
|
||||
$sequence['type'] = 'benchmark';
|
||||
return $sequence;
|
||||
});
|
||||
}
|
||||
|
||||
public function addBenchmark($benchMarks)
|
||||
{
|
||||
$benchMark = $this->getBlock();
|
||||
if ($benchMark) {
|
||||
$benchMark['type'] = 'benchmark';
|
||||
$benchMarks[$this->triggerName] = $benchMark;
|
||||
}
|
||||
|
||||
return $benchMarks;
|
||||
}
|
||||
|
||||
public function pushBlockFields($fields, $funnel)
|
||||
{
|
||||
$fields[$this->triggerName] = $this->getBlockFields($funnel);
|
||||
return $fields;
|
||||
}
|
||||
|
||||
public function getConditionDefaults($benchMark)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getConditionFields($benchMark)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function benchmarkTypeField()
|
||||
{
|
||||
return [
|
||||
'label' => __('Benchmark type', 'fluent-crm'),
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
[
|
||||
'id' => 'optional',
|
||||
'title' => __('[Optional Point] This is an optional trigger point', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'required',
|
||||
'title' => __('[Essential Point] Select IF this step is required for processing further actions', 'fluent-crm')
|
||||
]
|
||||
],
|
||||
'inline_help' => __('If you select [Optional Point] it will work as an Optional Trigger otherwise, it will wait for full-fill this action', 'fluent-crm')
|
||||
];
|
||||
}
|
||||
|
||||
public function canEnterField()
|
||||
{
|
||||
return [
|
||||
'type' => 'yes_no_check',
|
||||
'check_label' => __('Contacts can enter directly to this sequence point. If you enable this then any contact meet with goal will enter in this goal point.', 'fluent-crm')
|
||||
];
|
||||
}
|
||||
|
||||
public function assertCurrentGoalState($asserted, $benchmark, $funnelSubscriber)
|
||||
{
|
||||
return $asserted;
|
||||
}
|
||||
|
||||
abstract public function getBlock();
|
||||
|
||||
abstract public function getBlockFields($funnel);
|
||||
|
||||
abstract public function handle($benchMark, $originalArgs);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel;
|
||||
|
||||
abstract class BaseTrigger
|
||||
{
|
||||
|
||||
protected $triggerName;
|
||||
|
||||
protected $actionArgNum = 1;
|
||||
|
||||
protected $priority = 10;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->register();
|
||||
}
|
||||
|
||||
public function register()
|
||||
{
|
||||
add_filter('fluentcrm_funnel_triggers', array($this, 'addTrigger'), $this->priority, 1);
|
||||
|
||||
add_filter('fluentcrm_funnel_editor_details_' . $this->triggerName, array($this, 'prepareEditorDetails'), 10, 1);
|
||||
|
||||
add_action('fluentcrm_funnel_start_' . $this->triggerName, array($this, 'handle'), 10, 2);
|
||||
|
||||
add_filter('fluentcrm_funnel_arg_num_' . $this->triggerName, function ($num) {
|
||||
if ($num >= $this->actionArgNum) {
|
||||
return $num;
|
||||
}
|
||||
return $this->actionArgNum;
|
||||
});
|
||||
}
|
||||
|
||||
public function addTrigger($triggers)
|
||||
{
|
||||
$trigger = $this->getTrigger();
|
||||
|
||||
if (!$trigger) {
|
||||
return $triggers;
|
||||
}
|
||||
|
||||
$triggers[$this->triggerName] = $trigger;
|
||||
return $triggers;
|
||||
}
|
||||
|
||||
public function prepareEditorDetails($funnel)
|
||||
{
|
||||
$funnel->settings = wp_parse_args($funnel->settings, $this->getFunnelSettingsDefaults());
|
||||
|
||||
$settingsFields = $this->getSettingsFields($funnel);
|
||||
|
||||
$settingsFields['fields']['__force_run_actions'] = [
|
||||
'label' => '',
|
||||
'type' => 'yes_no_check',
|
||||
'check_label' => __('Run the automation actions even if the contact status is not subscribed', 'fluent-crm'),
|
||||
'true_label' => 'yes',
|
||||
'false_label' => 'no'
|
||||
];
|
||||
|
||||
$settingsFields['fields']['__force_run_actions_info'] = [
|
||||
'type' => 'html',
|
||||
'info' => '<em>' . __('The actions will run even the contact\'s status is not in subscribed status.', 'fluent-crm') . '</em>',
|
||||
'dependency' => [
|
||||
'depends_on' => '__force_run_actions',
|
||||
'operator' => '=',
|
||||
'value' => 'yes'
|
||||
]
|
||||
];
|
||||
|
||||
$funnel->settingsFields = $settingsFields;
|
||||
|
||||
$funnel->conditions = wp_parse_args($funnel->conditions, $this->getFunnelConditionDefaults($funnel));
|
||||
|
||||
$conditionFields = $this->getConditionFields($funnel);
|
||||
|
||||
$funnel->conditionFields = $conditionFields;
|
||||
return $funnel;
|
||||
}
|
||||
|
||||
public function getFunnelConditionDefaults($funnel)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getConditionFields($funnel)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
abstract public function getTrigger();
|
||||
|
||||
abstract public function getFunnelSettingsDefaults();
|
||||
|
||||
abstract public function getSettingsFields($funnel);
|
||||
|
||||
abstract public function handle($funnel, $originalArgs);
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel\Benchmarks;
|
||||
|
||||
use FluentCrm\App\Services\Funnel\BaseBenchMark;
|
||||
use FluentCrm\App\Services\Funnel\FunnelProcessor;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
class ListAppliedBenchmark extends BaseBenchMark
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->triggerName = 'fluentcrm_contact_added_to_lists';
|
||||
$this->actionArgNum = 2;
|
||||
$this->priority = 20;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getBlock()
|
||||
{
|
||||
return [
|
||||
'title' => __('List Applied', 'fluent-crm'),
|
||||
'description' => __('This will run when selected lists have been applied to a contact', 'fluent-crm'),
|
||||
'icon' => 'fc-icon-list_applied_2',//fluentCrmMix('images/funnel_icons/list_applied.svg'),
|
||||
'settings' => [
|
||||
'lists' => [],
|
||||
'select_type' => 'any',
|
||||
'type' => 'optional',
|
||||
'can_enter' => 'yes'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getDefaultSettings()
|
||||
{
|
||||
return [
|
||||
'lists' => [],
|
||||
'select_type' => 'any',
|
||||
'type' => 'optional',
|
||||
'can_enter' => 'yes'
|
||||
];
|
||||
}
|
||||
|
||||
public function getBlockFields($funnel)
|
||||
{
|
||||
return [
|
||||
'title' => __('List Applied', 'fluent-crm'),
|
||||
'sub_title' => __('This will run when selected lists have been applied to a contact', 'fluent-crm'),
|
||||
'fields' => [
|
||||
'lists' => [
|
||||
'type' => 'option_selectors',
|
||||
'option_key' => 'lists',
|
||||
'is_multiple' => true,
|
||||
'label' => __('Select Lists', 'fluent-crm'),
|
||||
'placeholder' => __('Select List', 'fluent-crm'),
|
||||
'creatable' => true
|
||||
],
|
||||
'select_type' => [
|
||||
'label' => __('Run When', 'fluent-crm'),
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
[
|
||||
'id' => 'any',
|
||||
'title' => __('contact added in any of the selected Lists', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'all',
|
||||
'title' => __('contact added in all of the selected lists', 'fluent-crm')
|
||||
]
|
||||
],
|
||||
'dependency' => [
|
||||
'depends_on' => 'lists',
|
||||
'operator' => '!=',
|
||||
'value' => []
|
||||
]
|
||||
],
|
||||
'type' => $this->benchmarkTypeField(),
|
||||
'can_enter' => $this->canEnterField()
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function handle($benchMark, $originalArgs)
|
||||
{
|
||||
$listIds = $originalArgs[0];
|
||||
$subscriber = $originalArgs[1];
|
||||
$settings = $benchMark->settings;
|
||||
|
||||
$benchmarkLists = Arr::get($settings, 'lists', []);
|
||||
|
||||
// Gate on the lists actually applied in this event. Without this a contact
|
||||
// who already belongs to the benchmark list would advance past the benchmark
|
||||
// on any unrelated list-add event.
|
||||
if (!$benchmarkLists || !array_intersect((array) $listIds, $benchmarkLists)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subscriberLists = $subscriber->lists->pluck('id')->toArray();
|
||||
|
||||
if (!$this->isListMatched($subscriberLists, $benchmarkLists, $settings)) {
|
||||
return; // not matched based on condition
|
||||
}
|
||||
|
||||
$funnelProcessor = new FunnelProcessor();
|
||||
$funnelProcessor->startFunnelFromSequencePoint($benchMark, $subscriber);
|
||||
}
|
||||
|
||||
private function isListMatched($subscriberLists, $benchmarkLists, $settings)
|
||||
{
|
||||
$matchType = Arr::get($settings, 'select_type');
|
||||
|
||||
if (empty($benchmarkLists)) {
|
||||
return false;
|
||||
}
|
||||
// find common elements between the two arrays
|
||||
$intersection = array_intersect($benchmarkLists, $subscriberLists);
|
||||
|
||||
if ($matchType === 'any') {
|
||||
// At least one funnel list id is available.
|
||||
$isMatched = !empty($intersection);
|
||||
} else {
|
||||
// All of the funnel list ids are present.
|
||||
$isMatched = count($intersection) === count($settings['lists']);
|
||||
}
|
||||
|
||||
return $isMatched;
|
||||
}
|
||||
|
||||
public function assertCurrentGoalState($asserted, $benchmark, $funnelSubscriber)
|
||||
{
|
||||
if (!$funnelSubscriber || !$funnelSubscriber->subscriber) {
|
||||
return $asserted;
|
||||
}
|
||||
|
||||
$subscriberLists = $funnelSubscriber->subscriber->lists->pluck('id')->toArray();
|
||||
$benchamrkLists = Arr::get($benchmark->settings, 'lists', []);
|
||||
|
||||
|
||||
$benchmarkSettings= $benchmark->settings;
|
||||
|
||||
return $this->isListMatched($subscriberLists, $benchamrkLists, $benchmarkSettings);
|
||||
}
|
||||
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel\Benchmarks;
|
||||
|
||||
use FluentCrm\App\Services\Funnel\BaseBenchMark;
|
||||
use FluentCrm\App\Services\Funnel\FunnelProcessor;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
class RemoveFromListBenchmark extends BaseBenchMark
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->triggerName = 'fluentcrm_contact_removed_from_lists';
|
||||
$this->actionArgNum = 2;
|
||||
$this->priority = 40;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getBlock()
|
||||
{
|
||||
return [
|
||||
'title' => __('List Removed', 'fluent-crm'),
|
||||
'description' => __('This will run when selected lists have been removed from a contact', 'fluent-crm'),
|
||||
'icon' => 'fc-icon-list_removed',//fluentCrmMix('images/funnel_icons/list_removed.svg'),
|
||||
'settings' => [
|
||||
'lists' => [],
|
||||
'select_type' => 'any',
|
||||
'type' => 'optional',
|
||||
'can_enter' => 'yes'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getDefaultSettings()
|
||||
{
|
||||
return [
|
||||
'lists' => [],
|
||||
'select_type' => 'any',
|
||||
'type' => 'optional',
|
||||
'can_enter' => 'yes'
|
||||
];
|
||||
}
|
||||
|
||||
public function getBlockFields($funnel)
|
||||
{
|
||||
return [
|
||||
'title' => __('List Removed From Contact', 'fluent-crm'),
|
||||
'sub_title' => __('This will run when selected lists have been removed from a contact', 'fluent-crm'),
|
||||
'fields' => [
|
||||
'lists' => [
|
||||
'type' => 'option_selectors',
|
||||
'option_key' => 'lists',
|
||||
'is_multiple' => true,
|
||||
'label' => __('Select Lists', 'fluent-crm'),
|
||||
'placeholder' => __('Select List', 'fluent-crm'),
|
||||
'creatable' => true
|
||||
],
|
||||
'select_type' => [
|
||||
'label' => __('Run When', 'fluent-crm'),
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
[
|
||||
'id' => 'any',
|
||||
'title' => __('contact removed from any of the selected Lists', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'all',
|
||||
'title' => __('contact removed from all of the selected lists', 'fluent-crm')
|
||||
]
|
||||
],
|
||||
'dependency' => [
|
||||
'depends_on' => 'lists',
|
||||
'operator' => '!=',
|
||||
'value' => []
|
||||
]
|
||||
],
|
||||
'type' => $this->benchmarkTypeField(),
|
||||
'can_enter' => $this->canEnterField()
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function handle($benchMark, $originalArgs)
|
||||
{
|
||||
$listIds = $originalArgs[0];
|
||||
$subscriber = $originalArgs[1];
|
||||
$settings = $benchMark->settings;
|
||||
|
||||
if (!$this->isListMatched($listIds, $subscriber, $settings)) {
|
||||
return; // not matched based on condition
|
||||
}
|
||||
|
||||
$funnelProcessor = new FunnelProcessor();
|
||||
$funnelProcessor->startFunnelFromSequencePoint($benchMark, $subscriber);
|
||||
}
|
||||
|
||||
private function isListMatched($listIds, $subscriber, $settings)
|
||||
{
|
||||
$benchmarkLists = Arr::get($settings, 'lists', []);
|
||||
if (empty($benchmarkLists)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$isMatched = array_intersect($benchmarkLists, $listIds);
|
||||
if (!$isMatched) {
|
||||
return false; // not in our scope
|
||||
}
|
||||
|
||||
$matchType = Arr::get($settings, 'select_type');
|
||||
|
||||
if ($matchType == 'all') {
|
||||
// Check that ALL benchmark lists have been removed (none remain on subscriber)
|
||||
$attachedListIds = $subscriber->lists->pluck('id')->toArray();
|
||||
return empty(array_intersect($benchmarkLists, $attachedListIds));
|
||||
}
|
||||
|
||||
return $isMatched;
|
||||
}
|
||||
|
||||
public function assertCurrentGoalState($asserted, $benchmark, $funnelSubscriber)
|
||||
{
|
||||
if (!$funnelSubscriber || !$funnelSubscriber->subscriber) {
|
||||
return $asserted;
|
||||
}
|
||||
|
||||
$subscriber = $funnelSubscriber->subscriber;
|
||||
$settings = $benchmark->settings;
|
||||
$benchmarkLists = Arr::get($settings, 'lists', []);
|
||||
|
||||
if (empty($benchmarkLists)) {
|
||||
return $asserted;
|
||||
}
|
||||
|
||||
$attachedListIds = $subscriber->lists->pluck('id')->toArray();
|
||||
$matchType = Arr::get($settings, 'select_type');
|
||||
|
||||
if ($matchType == 'all') {
|
||||
return empty(array_intersect($benchmarkLists, $attachedListIds));
|
||||
}
|
||||
|
||||
// 'any' — at least one benchmark list is NOT in subscriber's lists
|
||||
$removed = array_diff($benchmarkLists, $attachedListIds);
|
||||
return !empty($removed);
|
||||
}
|
||||
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel\Benchmarks;
|
||||
|
||||
use FluentCrm\App\Services\Funnel\BaseBenchMark;
|
||||
use FluentCrm\App\Services\Funnel\FunnelProcessor;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
class RemoveFromTagBenchmark extends BaseBenchMark
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->triggerName = 'fluentcrm_contact_removed_from_tags';
|
||||
$this->actionArgNum = 2;
|
||||
$this->priority = 20;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getBlock()
|
||||
{
|
||||
return [
|
||||
'title' => __('Tag Removed', 'fluent-crm'),
|
||||
'description' => __('This will run when selected Tags have been removed from a contact', 'fluent-crm'),
|
||||
'icon' => 'fc-icon-tag_removed',//fluentCrmMix('images/funnel_icons/tag_removed.svg'),
|
||||
'settings' => [
|
||||
'tags' => [],
|
||||
'select_type' => 'any',
|
||||
'type' => 'optional',
|
||||
'can_enter' => 'yes'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getDefaultSettings()
|
||||
{
|
||||
return [
|
||||
'tags' => [],
|
||||
'select_type' => 'any',
|
||||
'type' => 'optional',
|
||||
'can_enter' => 'yes'
|
||||
];
|
||||
}
|
||||
|
||||
public function getBlockFields($funnel)
|
||||
{
|
||||
return [
|
||||
'title' => __('Tag Removed From Contact', 'fluent-crm'),
|
||||
'sub_title' => __('This will run when selected Tags have been removed from a contact', 'fluent-crm'),
|
||||
'fields' => [
|
||||
'tags' => [
|
||||
'type' => 'option_selectors',
|
||||
'option_key' => 'tags',
|
||||
'is_multiple' => true,
|
||||
'label' => __('Select Tags', 'fluent-crm'),
|
||||
'placeholder' => __('Select a Tag', 'fluent-crm'),
|
||||
'creatable' => true
|
||||
],
|
||||
'select_type' => [
|
||||
'label' => __('Run When', 'fluent-crm'),
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
[
|
||||
'id' => 'any',
|
||||
'title' => __('Run if any selected tag removed from a contact', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'all',
|
||||
'title' => __('Need all selected tags removed from the contact', 'fluent-crm')
|
||||
]
|
||||
],
|
||||
'dependency' => [
|
||||
'depends_on' => 'tags',
|
||||
'operator' => '!=',
|
||||
'value' => []
|
||||
]
|
||||
],
|
||||
'type' => $this->benchmarkTypeField(),
|
||||
'can_enter' => $this->canEnterField()
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function handle($benchMark, $originalArgs)
|
||||
{
|
||||
$listIds = $originalArgs[0];
|
||||
$subscriber = $originalArgs[1];
|
||||
$settings = $benchMark->settings;
|
||||
|
||||
if (!$this->isTagMatched($listIds, $subscriber, $settings)) {
|
||||
return; // not matched based on condition
|
||||
}
|
||||
|
||||
$funnelProcessor = new FunnelProcessor();
|
||||
$funnelProcessor->startFunnelFromSequencePoint($benchMark, $subscriber);
|
||||
}
|
||||
|
||||
private function isTagMatched($tagIds, $subscriber, $settings)
|
||||
{
|
||||
$benchmarkTags = Arr::get($settings, 'tags', []);
|
||||
if (empty($benchmarkTags)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$isMatched = array_intersect($benchmarkTags, $tagIds);
|
||||
if (!$isMatched) {
|
||||
return false; // not in our scope
|
||||
}
|
||||
|
||||
$matchType = Arr::get($settings, 'select_type');
|
||||
|
||||
if ($matchType == 'all') {
|
||||
// Check that ALL benchmark tags have been removed (none remain on subscriber)
|
||||
$attachedTagIds = $subscriber->tags->pluck('id')->toArray();
|
||||
return empty(array_intersect($benchmarkTags, $attachedTagIds));
|
||||
}
|
||||
|
||||
return $isMatched;
|
||||
}
|
||||
|
||||
public function assertCurrentGoalState($asserted, $benchmark, $funnelSubscriber)
|
||||
{
|
||||
if (!$funnelSubscriber || !$funnelSubscriber->subscriber) {
|
||||
return $asserted;
|
||||
}
|
||||
|
||||
$subscriber = $funnelSubscriber->subscriber;
|
||||
$settings = $benchmark->settings;
|
||||
$benchmarkTags = Arr::get($settings, 'tags', []);
|
||||
|
||||
if (empty($benchmarkTags)) {
|
||||
return $asserted;
|
||||
}
|
||||
|
||||
$attachedTagIds = $subscriber->tags->pluck('id')->toArray();
|
||||
$matchType = Arr::get($settings, 'select_type');
|
||||
|
||||
if ($matchType == 'all') {
|
||||
return empty(array_intersect($benchmarkTags, $attachedTagIds));
|
||||
}
|
||||
|
||||
// 'any' — at least one benchmark tag is NOT in subscriber's tags
|
||||
$removed = array_diff($benchmarkTags, $attachedTagIds);
|
||||
return !empty($removed);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel\Benchmarks;
|
||||
|
||||
use FluentCrm\App\Services\Funnel\BaseBenchMark;
|
||||
use FluentCrm\App\Services\Funnel\FunnelProcessor;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
class TagAppliedBenchmark extends BaseBenchMark
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->triggerName = 'fluentcrm_contact_added_to_tags';
|
||||
$this->actionArgNum = 2;
|
||||
$this->priority = 20;
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getBlock()
|
||||
{
|
||||
return [
|
||||
'title' => __('Tag Applied', 'fluent-crm'),
|
||||
'description' => __('This will run when selected Tags have been applied to a contact', 'fluent-crm'),
|
||||
'icon' => 'fc-icon-tag_applied',//fluentCrmMix('images/funnel_icons/tag-applied.svg'),
|
||||
'settings' => [
|
||||
'tags' => [],
|
||||
'select_type' => 'any',
|
||||
'type' => 'optional',
|
||||
'can_enter' => 'yes'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getDefaultSettings()
|
||||
{
|
||||
return [
|
||||
'tags' => [],
|
||||
'select_type' => 'any',
|
||||
'type' => 'optional',
|
||||
'can_enter' => 'yes'
|
||||
];
|
||||
}
|
||||
|
||||
public function getBlockFields($funnel)
|
||||
{
|
||||
return [
|
||||
'title' => __('Tag Applied', 'fluent-crm'),
|
||||
'sub_title' => __('This will run when selected Tags have been applied to a contact', 'fluent-crm'),
|
||||
'fields' => [
|
||||
'tags' => [
|
||||
'type' => 'option_selectors',
|
||||
'option_key' => 'tags',
|
||||
'creatable' => true,
|
||||
'is_multiple' => true,
|
||||
'label' => __('Select Tags', 'fluent-crm'),
|
||||
'placeholder' => __('Select Tags', 'fluent-crm'),
|
||||
],
|
||||
'select_type' => [
|
||||
'label' => __('Run When', 'fluent-crm'),
|
||||
'type' => 'radio',
|
||||
'options' => [
|
||||
[
|
||||
'id' => 'any',
|
||||
'title' => __('contact added in any of the selected Tags', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'all',
|
||||
'title' => __('contact added in all of the selected Tags', 'fluent-crm')
|
||||
]
|
||||
],
|
||||
'dependency' => [
|
||||
'depends_on' => 'tags',
|
||||
'operator' => '!=',
|
||||
'value' => []
|
||||
]
|
||||
],
|
||||
'type' => $this->benchmarkTypeField(),
|
||||
'can_enter' => $this->canEnterField()
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function handle($benchMark, $originalArgs)
|
||||
{
|
||||
$tagIds = $originalArgs[0];
|
||||
$subscriber = $originalArgs[1];
|
||||
$settings = $benchMark->settings;
|
||||
|
||||
$benchmarkTags = Arr::get($settings, 'tags', []);
|
||||
|
||||
// Gate on the tags actually applied in this event. Without this a contact
|
||||
// who already holds the benchmark tag would advance past the benchmark
|
||||
// on any unrelated tag-add event.
|
||||
if (!$benchmarkTags || !array_intersect((array) $tagIds, $benchmarkTags)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subscriberTags = $subscriber->tags->pluck('id')->toArray();
|
||||
|
||||
if (!$this->isTagMatched($subscriberTags, $benchmarkTags, $settings)) {
|
||||
return; // not matched based on condition
|
||||
}
|
||||
|
||||
$funnelProcessor = new FunnelProcessor();
|
||||
$funnelProcessor->startFunnelFromSequencePoint($benchMark, $subscriber);
|
||||
}
|
||||
|
||||
private function isTagMatched($subscriberTags, $benchmarkTags, $settings)
|
||||
{
|
||||
$matchType = Arr::get($settings, 'select_type');
|
||||
|
||||
if (empty($benchmarkTags)) {
|
||||
return false;
|
||||
}
|
||||
// find common elements between the two arrays
|
||||
$intersection = array_intersect($benchmarkTags, $subscriberTags);
|
||||
|
||||
if ($matchType === 'any') {
|
||||
// At least one funnel list id is available.
|
||||
$isMatched = !empty($intersection);
|
||||
} else {
|
||||
// All of the funnel list ids are present.
|
||||
$isMatched = count($intersection) === count($settings['tags']);
|
||||
}
|
||||
|
||||
return $isMatched;
|
||||
}
|
||||
|
||||
|
||||
public function assertCurrentGoalState($asserted, $benchmark, $funnelSubscriber)
|
||||
{
|
||||
if (!$funnelSubscriber || !$funnelSubscriber->subscriber) {
|
||||
return $asserted;
|
||||
}
|
||||
|
||||
$subscriberTags = $funnelSubscriber->subscriber->tags->pluck('id')->toArray();
|
||||
$benchmarkTags = Arr::get($benchmark->settings, 'tags', []);
|
||||
|
||||
$benchmarkSettings= $benchmark->settings;
|
||||
|
||||
return $this->isTagMatched($subscriberTags, $benchmarkTags, $benchmarkSettings);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,995 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel;
|
||||
|
||||
use FluentCrm\App\Hooks\Handlers\FunnelHandler;
|
||||
use FluentCrm\App\Models\Funnel;
|
||||
use FluentCrm\App\Models\FunnelMetric;
|
||||
use FluentCrm\App\Models\FunnelSequence;
|
||||
use FluentCrm\App\Models\FunnelSubscriber;
|
||||
use FluentCrm\App\Models\Subscriber;
|
||||
use FluentCrm\App\Services\Helper;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
class FunnelHelper
|
||||
{
|
||||
public static function changeFunnelSubSequenceStatus($funnelSubId, $sequenceId, $status = 'complete')
|
||||
{
|
||||
return FunnelSubscriber::where('id', $funnelSubId)
|
||||
->update([
|
||||
'last_sequence_status' => $status,
|
||||
'last_sequence_id' => $sequenceId,
|
||||
'last_executed_time' => current_time('mysql')
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getUpdateOptions()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'id' => 'update',
|
||||
'title' => __('Update if Exist', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'skip_all_if_exist',
|
||||
'title' => __('Skip this automation if contact already exist', 'fluent-crm')
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public static function prepareUserData($user)
|
||||
{
|
||||
$subscriber = Helper::getWPMapUserInfo($user);
|
||||
$subscriber['source'] = 'web';
|
||||
return $subscriber;
|
||||
}
|
||||
|
||||
public static function getSubscriber($emailOrUserId)
|
||||
{
|
||||
$column = 'email';
|
||||
if (is_int($emailOrUserId)) {
|
||||
$column = 'user_id';
|
||||
}
|
||||
|
||||
return Subscriber::where($column, $emailOrUserId)->first();
|
||||
}
|
||||
|
||||
public static function createOrUpdateContact($data)
|
||||
{
|
||||
return FluentCrmApi('contacts')->createOrUpdate($data);
|
||||
}
|
||||
|
||||
public static function getUserRoles($keyed = false)
|
||||
{
|
||||
if (!function_exists('get_editable_roles')) {
|
||||
require_once(ABSPATH . '/wp-admin/includes/user.php');
|
||||
}
|
||||
|
||||
$roles = \get_editable_roles();
|
||||
$formattedRoles = [];
|
||||
foreach ($roles as $roleKey => $role) {
|
||||
if ($keyed) {
|
||||
$formattedRoles[$roleKey] = $role['name'];
|
||||
} else {
|
||||
$formattedRoles[] = [
|
||||
'id' => $roleKey,
|
||||
'title' => $role['name']
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
return $formattedRoles;
|
||||
}
|
||||
|
||||
public static function ifAlreadyInFunnel($funnelId, $subscriberId)
|
||||
{
|
||||
return FunnelSubscriber::where('funnel_id', $funnelId)
|
||||
->where('subscriber_id', $subscriberId)
|
||||
->first();
|
||||
}
|
||||
|
||||
public static function maybeExplodeFullName($data)
|
||||
{
|
||||
if (empty($data['first_name']) && empty($data['last_name'])) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if (empty($data['first_name']) || !empty($data['last_name'])) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$fullNameArray = explode(' ', $data['first_name']);
|
||||
$data['first_name'] = array_shift($fullNameArray);
|
||||
if ($fullNameArray) {
|
||||
$data['last_name'] = implode(' ', $fullNameArray);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public static function syncTags($subscriber, $tags = [])
|
||||
{
|
||||
if ($tags) {
|
||||
$subscriber->attachTags($tags);
|
||||
}
|
||||
}
|
||||
|
||||
public static function syncLists($subscriber, $lists = [])
|
||||
{
|
||||
// Syncing
|
||||
if ($lists) {
|
||||
$subscriber->attachLists($lists);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getPrimaryContactFieldMaps()
|
||||
{
|
||||
return [
|
||||
'first_name' => [
|
||||
'type' => 'value_options',
|
||||
'label' => __('First Name', 'fluent-crm')
|
||||
],
|
||||
'last_name' => [
|
||||
'type' => 'value_options',
|
||||
'label' => __('Last Name', 'fluent-crm')
|
||||
],
|
||||
'email' => [
|
||||
'type' => 'value_options',
|
||||
'label' => __('Email', 'fluent-crm')
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public static function getSecondaryContactFieldMaps()
|
||||
{
|
||||
$mainFields = [
|
||||
'prefix' => [
|
||||
'type' => 'value_options',
|
||||
'label' => __('Name Prefix', 'fluent-crm')
|
||||
],
|
||||
'address_line_1' => [
|
||||
'type' => 'value_options',
|
||||
'label' => __('Address Line 1', 'fluent-crm')
|
||||
],
|
||||
'address_line_2' => [
|
||||
'type' => 'value_options',
|
||||
'label' => __('Address Line 2', 'fluent-crm')
|
||||
],
|
||||
'postal_code' => [
|
||||
'type' => 'value_options',
|
||||
'label' => __('Postal Code', 'fluent-crm')
|
||||
],
|
||||
'city' => [
|
||||
'type' => 'value_options',
|
||||
'label' => __('City', 'fluent-crm')
|
||||
],
|
||||
'state' => [
|
||||
'type' => 'value_options',
|
||||
'label' => __('State', 'fluent-crm')
|
||||
],
|
||||
'country' => [
|
||||
'type' => 'value_options',
|
||||
'label' => __('country', 'fluent-crm')
|
||||
],
|
||||
'phone' => [
|
||||
'type' => 'value_options',
|
||||
'label' => __('Phone', 'fluent-crm')
|
||||
]
|
||||
];
|
||||
|
||||
$customFields = fluentcrm_get_option('contact_custom_fields', []);
|
||||
if ($customFields) {
|
||||
foreach ($customFields as $item) {
|
||||
$mainFields['custom.' . $item['slug']] = [
|
||||
'type' => 'value_options',
|
||||
'label' => $item['label']
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $mainFields;
|
||||
|
||||
}
|
||||
|
||||
public static function removeSubscribersFromFunnel($funnelId, $subscriberIds)
|
||||
{
|
||||
FunnelSubscriber::where('funnel_id', $funnelId)
|
||||
->whereIn('subscriber_id', $subscriberIds)
|
||||
->delete();
|
||||
|
||||
FunnelMetric::where('funnel_id', $funnelId)
|
||||
->whereIn('subscriber_id', $subscriberIds)
|
||||
->delete();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function saveFunnelSequence($funnelId, $data)
|
||||
{
|
||||
$funnelSettings = \json_decode(Arr::get($data, 'funnel_settings'), true);
|
||||
$funnelConditions = \json_decode(Arr::get($data, 'conditions', '[]'), true);
|
||||
|
||||
$funnel = Funnel::findOrFail($funnelId);
|
||||
|
||||
if (is_array($funnelSettings)) {
|
||||
$funnel->settings = $funnelSettings;
|
||||
}
|
||||
|
||||
if ($funnelTitle = Arr::get($data, 'funnel_title')) {
|
||||
$funnel->title = sanitize_text_field($funnelTitle);
|
||||
}
|
||||
|
||||
if (is_array($funnelConditions)) {
|
||||
$funnel->conditions = $funnelConditions;
|
||||
}
|
||||
$funnel->status = Arr::get($data, 'status');
|
||||
// Always write updated_at so it reflects when sequences were last saved
|
||||
$funnel->updated_at = current_time('mysql');
|
||||
$funnel->save();
|
||||
|
||||
if ($funnelDescription = Arr::get($data, 'funnel_description')) {
|
||||
$funnel->updateMeta('description', $funnelDescription);
|
||||
} else {
|
||||
$funnel->deleteMeta('description');
|
||||
}
|
||||
|
||||
$sequences = \json_decode(Arr::get($data, 'sequences', '[]'), true) ?? [];
|
||||
|
||||
if (!is_array($sequences)) {
|
||||
return $funnel;
|
||||
}
|
||||
|
||||
$sequenceIds = [];
|
||||
$cDelay = 0;
|
||||
$delay = 0;
|
||||
|
||||
$indexCount = 0;
|
||||
|
||||
foreach ($sequences as $index => $sequence) {
|
||||
// it's creatable
|
||||
$sequence['funnel_id'] = $funnel->id;
|
||||
$sequence['status'] = 'published';
|
||||
$sequence['conditions'] = [];
|
||||
$sequence['sequence'] = $indexCount + 1;
|
||||
$sequence['c_delay'] = $cDelay;
|
||||
$sequence['delay'] = $delay;
|
||||
$delay = 0;
|
||||
|
||||
$actionName = $sequence['action_name'];
|
||||
|
||||
if ($actionName == 'fluentcrm_wait_times') {
|
||||
$delay = self::getDelayInSecond($sequence['settings']);
|
||||
$cDelay += $delay;
|
||||
}
|
||||
|
||||
$sequence = apply_filters('fluentcrm_funnel_sequence_saving_' . $sequence['action_name'], $sequence, $funnel);
|
||||
|
||||
if (Arr::get($sequence, 'type') == 'benchmark') {
|
||||
$delay = $sequence['delay'];
|
||||
}
|
||||
|
||||
$sequence['id'] = self::createOrUpdateSequence($sequence);
|
||||
$sequenceIds[] = $sequence['id'];
|
||||
|
||||
// We have to handle the children if it's conditional block
|
||||
if ($sequence['type'] == 'conditional') {
|
||||
$childIds = self::saveChildSequences($sequence, $funnel);
|
||||
$indexCount += count($childIds);
|
||||
$sequenceIds = array_unique(array_merge($sequenceIds, $childIds));
|
||||
}
|
||||
|
||||
$indexCount += 1;
|
||||
}
|
||||
|
||||
if ($sequenceIds) {
|
||||
$deletingSequences = FunnelSequence::whereNotIn('id', $sequenceIds)
|
||||
->where('funnel_id', $funnel->id)
|
||||
->get();
|
||||
} else {
|
||||
$deletingSequences = FunnelSequence::where('funnel_id', $funnel->id)->get();
|
||||
}
|
||||
|
||||
if ($deletingSequences->count()) {
|
||||
foreach ($deletingSequences as $deletingSequence) {
|
||||
do_action('fluentcrm_funnel_sequence_deleting_' . $deletingSequence->action_name, $deletingSequence, $funnel);
|
||||
$deletingSequence->delete();
|
||||
}
|
||||
|
||||
// Unstick subscribers waiting on deleted benchmark sequences
|
||||
FunnelSubscriber::where('funnel_id', $funnel->id)
|
||||
->where('status', 'waiting')
|
||||
->whereNotIn('next_sequence_id', $sequenceIds)
|
||||
->update([
|
||||
'status' => 'active',
|
||||
'next_execution_time' => current_time('mysql')
|
||||
]);
|
||||
}
|
||||
|
||||
// Sync next_sequence integer after renumbering — active/waiting subscribers
|
||||
// may have stale values from before the re-save
|
||||
global $wpdb;
|
||||
$subscribersTable = $wpdb->prefix . 'fc_funnel_subscribers';
|
||||
$sequencesTable = $wpdb->prefix . 'fc_funnel_sequences';
|
||||
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$subscribersTable} fs
|
||||
JOIN {$sequencesTable} seq ON fs.next_sequence_id = seq.id
|
||||
SET fs.next_sequence = seq.sequence
|
||||
WHERE fs.funnel_id = %d
|
||||
AND fs.status IN ('active', 'waiting')
|
||||
AND (fs.next_sequence IS NULL OR fs.next_sequence != seq.sequence)",
|
||||
$funnel->id
|
||||
));
|
||||
|
||||
(new FunnelHandler())->resetFunnelIndexes();
|
||||
|
||||
return $funnel;
|
||||
}
|
||||
|
||||
private static function createOrUpdateSequence($sequence)
|
||||
{
|
||||
if (empty($sequence['id'])) {
|
||||
$sequence['created_by'] = get_current_user_id();
|
||||
$createdSequence = FunnelSequence::create($sequence);
|
||||
do_action('fluent_crm/sequence_created_' . $createdSequence->action_name, $createdSequence);
|
||||
return $createdSequence->id;
|
||||
}
|
||||
|
||||
$sequence['updated_at'] = current_time('mysql');
|
||||
$sequence['settings'] = \maybe_serialize($sequence['settings']);
|
||||
$sequence['conditions'] = \maybe_serialize($sequence['conditions']);
|
||||
|
||||
$sequenceId = $sequence['id'];
|
||||
$data = Arr::only($sequence, (new FunnelSequence())->getFillable());
|
||||
|
||||
FunnelSequence::where('id', $sequenceId)->update($data);
|
||||
|
||||
return $sequenceId;
|
||||
}
|
||||
|
||||
public static function getDelayInSecond($settings)
|
||||
{
|
||||
$waitType = Arr::get($settings, 'wait_type');
|
||||
|
||||
if (!$waitType && Arr::get($settings, 'is_timestamp_wait') == 'yes') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($waitType == 'timestamp_wait' || $waitType == 'to_day') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$unit = Arr::get($settings, 'wait_time_unit');
|
||||
$time = (int) Arr::get($settings, 'wait_time_amount');
|
||||
|
||||
if ($unit == 'months') {
|
||||
// Months are not a fixed number of seconds, so anchor to the
|
||||
// current time to get a calendar-accurate offset (e.g. +2 months).
|
||||
$now = current_time('timestamp');
|
||||
$delay = strtotime('+' . $time . ' months', $now) - $now;
|
||||
} else {
|
||||
$converter = 86400; // default day
|
||||
if ($unit == 'hours') {
|
||||
$converter = 3600; // hour
|
||||
} else if ($unit == 'minutes') {
|
||||
$converter = 60;
|
||||
}
|
||||
$delay = $time * $converter;
|
||||
}
|
||||
|
||||
if (!$delay || $delay < 1) {
|
||||
$delay = 1;
|
||||
}
|
||||
|
||||
return $delay;
|
||||
}
|
||||
|
||||
public static function getCurrentDelayInSeconds($settings, $sequence = null, $funnelSubId = null)
|
||||
{
|
||||
$waitType = Arr::get($settings, 'wait_type');
|
||||
|
||||
/*
|
||||
* For Specific Date and Time
|
||||
*/
|
||||
if ((!$waitType && Arr::get($settings, 'is_timestamp_wait') == 'yes') || $waitType == 'timestamp_wait') {
|
||||
$timeStamp = current_time('timestamp');
|
||||
$waitTimes = strtotime(Arr::get($settings, 'wait_date_time'), $timeStamp) - $timeStamp;
|
||||
if ($waitTimes < 1) {
|
||||
$waitTimes = 0;
|
||||
}
|
||||
return apply_filters('fluent_crm/funnel_seq_delay_in_seconds', $waitTimes, $settings, $sequence, $funnelSubId);
|
||||
}
|
||||
|
||||
if ($waitType && $waitType == 'to_day') {
|
||||
$nextDays = Arr::get($settings, 'to_day', []);
|
||||
$timeStampNow = current_time('timestamp');
|
||||
|
||||
$nextDays = array_map(function ($dayName) {
|
||||
return substr($dayName, 0, 3);
|
||||
}, $nextDays);
|
||||
|
||||
if (empty($nextDays)) { // if no day is selected
|
||||
$nextDays = [gmdate('D', $timeStampNow), gmdate('D', strtotime('+1 day', $timeStampNow))];
|
||||
}
|
||||
|
||||
$nextTime = Arr::get($settings, 'to_day_time');
|
||||
if (!$nextTime) {
|
||||
$nextTime = gmdate('H:i', $timeStampNow);
|
||||
}
|
||||
|
||||
$date = self::getEarliestDay($nextDays, $nextTime);
|
||||
|
||||
$seconds = strtotime($date) - current_time('timestamp');
|
||||
$waitTimes = ($seconds < 1) ? 0 : $seconds;
|
||||
return apply_filters('fluent_crm/funnel_seq_delay_in_seconds', $waitTimes, $settings, $sequence, $funnelSubId);
|
||||
}
|
||||
|
||||
|
||||
if ($waitType == 'by_custom_field') {
|
||||
if (!$funnelSubId) {
|
||||
return apply_filters('fluent_crm/funnel_seq_delay_in_seconds', 60, $settings, $sequence, $funnelSubId);
|
||||
}
|
||||
|
||||
$funnelSub = FunnelSubscriber::where('id', $funnelSubId)->first();
|
||||
|
||||
if (!$funnelSub || !$funnelSub->subscriber) {
|
||||
return apply_filters('fluent_crm/funnel_seq_delay_in_seconds', 60, $settings, $sequence, $funnelSubId);
|
||||
}
|
||||
|
||||
$customFieldKey = Arr::get($settings, 'by_custom_field', '');
|
||||
|
||||
if (!$customFieldKey) {
|
||||
return apply_filters('fluent_crm/funnel_seq_delay_in_seconds', 60, $settings, $sequence, $funnelSubId);
|
||||
}
|
||||
|
||||
$dateTime = null;
|
||||
|
||||
if ($customFieldKey == '__date_of_birth__') {
|
||||
$dateTime = $funnelSub->subscriber->date_of_birth;
|
||||
if ($dateTime && !self::isValidYmd($dateTime)) {
|
||||
$dateTime = null;
|
||||
}
|
||||
if ($dateTime) {
|
||||
// should be this current year's date
|
||||
$dateTime = gmdate('Y') . '-' . gmdate('m-d', strtotime($dateTime));
|
||||
|
||||
// if the date is passed, then next year
|
||||
if (strtotime($dateTime) < current_time('timestamp')) {
|
||||
$dateTime = (gmdate('Y') + 1) . '-' . gmdate('m-d', strtotime($dateTime));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$meta = $funnelSub->subscriber->custom_field_meta()->where('key', $customFieldKey)->first();
|
||||
if ($meta) {
|
||||
$dateTime = $meta->value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$dateTime) {
|
||||
return apply_filters('fluent_crm/funnel_seq_delay_in_seconds', 60, $settings, $sequence, $funnelSubId);
|
||||
}
|
||||
|
||||
$timeStamp = strtotime($dateTime);
|
||||
|
||||
$waitTimes = $timeStamp - current_time('timestamp');
|
||||
|
||||
if ($waitTimes < 1) {
|
||||
$waitTimes = 60;
|
||||
}
|
||||
|
||||
return apply_filters('fluent_crm/funnel_seq_delay_in_seconds', $waitTimes, $settings, $sequence, $funnelSubId);
|
||||
}
|
||||
|
||||
$unit = Arr::get($settings, 'wait_time_unit');
|
||||
$time = (int) Arr::get($settings, 'wait_time_amount');
|
||||
|
||||
if ($unit == 'months') {
|
||||
// Months are not a fixed number of seconds, so anchor to the
|
||||
// current time to get a calendar-accurate offset (e.g. +2 months).
|
||||
$now = current_time('timestamp');
|
||||
$waitTimes = strtotime('+' . $time . ' months', $now) - $now;
|
||||
} else {
|
||||
$converter = 86400; // default day
|
||||
if ($unit == 'hours') {
|
||||
$converter = 3600; // hour
|
||||
} else if ($unit == 'minutes') {
|
||||
$converter = 60;
|
||||
}
|
||||
$waitTimes = $time * $converter;
|
||||
}
|
||||
|
||||
if (!$waitTimes || $waitTimes < 1) {
|
||||
$waitTimes = 1;
|
||||
}
|
||||
|
||||
return apply_filters('fluent_crm/funnel_seq_delay_in_seconds', $waitTimes, $settings, $sequence, $funnelSubId);
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the next earliest day provided to $days array as ['Mon', Wed, 'Fri']
|
||||
* @param array $days
|
||||
* @return string $earliest
|
||||
*/
|
||||
private static function getEarliestDay($days, $time = '')
|
||||
{
|
||||
$timestamp = current_time('timestamp');
|
||||
$timeStampsArray = [];
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$timeStampsArray[] = $timestamp + ($i * 86400);
|
||||
}
|
||||
|
||||
$earliest = gmdate('Y-m-d ' . $time . ':s', $timestamp);
|
||||
|
||||
foreach ($timeStampsArray as $timeStampVal) {
|
||||
if (in_array(gmdate('D', $timeStampVal), $days)) {
|
||||
$earliest = gmdate('Y-m-d ' . $time . ':s', $timeStampVal);
|
||||
if (strtotime($earliest) - $timestamp > -60) {
|
||||
return $earliest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $earliest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is a valid calendar date in Y-m-d format (avoids strtotime normalizing invalid dates).
|
||||
*
|
||||
* @param string $ymd Date string (e.g. 2024-02-31).
|
||||
* @return bool
|
||||
*/
|
||||
private static function isValidYmd($ymd)
|
||||
{
|
||||
if (!is_string($ymd) || !preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $ymd, $parts)) {
|
||||
return false;
|
||||
}
|
||||
return checkdate((int) $parts[2], (int) $parts[3], (int) $parts[1]);
|
||||
}
|
||||
|
||||
private static function saveChildSequences($sequence, $funnel)
|
||||
{
|
||||
$sequenceIds = [];
|
||||
$indexCount = $sequence['sequence'];
|
||||
$childCats = Arr::get($sequence, 'children');
|
||||
foreach ($childCats as $category => $blocks) {
|
||||
$childDelay = 0;
|
||||
$childCDelay = 0;
|
||||
foreach ($blocks as $childIndex => $childSequence) {
|
||||
$childSequence['funnel_id'] = $funnel->id;
|
||||
$childSequence['status'] = 'published';
|
||||
$childSequence['parent_id'] = $sequence['id'];
|
||||
$childSequence['condition_type'] = $category;
|
||||
$childSequence['conditions'] = [];
|
||||
$childSequence['sequence'] = $indexCount + 1;
|
||||
$childSequence['c_delay'] = $sequence['c_delay'] + $childCDelay;
|
||||
$childSequence['delay'] = $sequence['delay'] + $childDelay;
|
||||
|
||||
$childDelay = 0;
|
||||
|
||||
/*
|
||||
* For Delay Calculation
|
||||
*/
|
||||
$actionName = $childSequence['action_name'];
|
||||
if ($actionName == 'fluentcrm_wait_times') {
|
||||
$childDelay = self::getDelayInSecond($childSequence['settings']);
|
||||
$childCDelay += $childDelay;
|
||||
}
|
||||
|
||||
$childSequence = apply_filters('fluentcrm_funnel_sequence_saving_' . $childSequence['action_name'], $childSequence, $funnel);
|
||||
$sequenceIds[] = self::createOrUpdateSequence($childSequence);
|
||||
$indexCount += 1;
|
||||
}
|
||||
}
|
||||
return $sequenceIds;
|
||||
}
|
||||
|
||||
public static function getFunnelSequences($funnel, $isFiltered = false)
|
||||
{
|
||||
$sequences = FunnelSequence::where('funnel_id', $funnel->id)
|
||||
->orderBy('sequence', 'ASC')
|
||||
->get();
|
||||
|
||||
if (!$isFiltered) {
|
||||
return $sequences;
|
||||
}
|
||||
|
||||
$formattedSequences = [];
|
||||
foreach ($sequences as $sequence) {
|
||||
$sequenceArray = $sequence->toArray();
|
||||
$formattedSequences[] = apply_filters('fluentcrm_funnel_sequence_filtered_' . $sequence->action_name, $sequenceArray, $funnel);
|
||||
}
|
||||
|
||||
return $formattedSequences;
|
||||
}
|
||||
|
||||
public static function maybeMigrateConditions($funnelId)
|
||||
{
|
||||
$conditionSequences = FunnelSequence::where('funnel_id', $funnelId)
|
||||
->where('type', 'conditional')
|
||||
->where('action_name', '!=', 'funnel_condition')
|
||||
->get();
|
||||
|
||||
foreach ($conditionSequences as $conditionSequence) {
|
||||
self::migrateConditionSequence($conditionSequence);
|
||||
}
|
||||
|
||||
return !$conditionSequences->isEmpty();
|
||||
}
|
||||
|
||||
public static function migrateConditionSequence($sequence, $dryRun = false)
|
||||
{
|
||||
$conditionalBlocks = [
|
||||
'funnel_condition',
|
||||
'funnel_ab_testing'
|
||||
];
|
||||
|
||||
if ($sequence->type != 'conditional' || in_array($sequence->action_name, $conditionalBlocks)) {
|
||||
return $sequence;
|
||||
}
|
||||
|
||||
$simpleMaps = [
|
||||
'fcrm_has_contact_tag' => [
|
||||
'source' => ['segment', 'tags'],
|
||||
'operator' => 'in',
|
||||
'value_access' => 'tags',
|
||||
'default_value' => [],
|
||||
],
|
||||
'fcrm_has_contact_list' => [
|
||||
'source' => ['segment', 'lists'],
|
||||
'operator' => 'in',
|
||||
'value_access' => 'lists',
|
||||
'default_value' => [],
|
||||
],
|
||||
'fcrm_has_user_role' => [
|
||||
'source' => ['segment', 'user_role'],
|
||||
'operator' => 'in',
|
||||
'value_access' => 'roles',
|
||||
'default_value' => [],
|
||||
],
|
||||
'fcrm_woo_is_purchased' => [
|
||||
'source' => ['woo', 'purchased_items'],
|
||||
'operator' => 'in',
|
||||
'value_access' => 'product_ids',
|
||||
'default_value' => [],
|
||||
],
|
||||
'fcrm_wishlist_is_in_level' => [
|
||||
'source' => ['wishlist', 'in_membership'],
|
||||
'operator' => 'in',
|
||||
'value_access' => 'level_ids',
|
||||
'default_value' => [],
|
||||
],
|
||||
'fcrm_tutor_is_in_course' => [
|
||||
'source' => ['tutorlms', 'is_in_course'],
|
||||
'operator' => 'in',
|
||||
'value_access' => 'course_ids',
|
||||
'default_value' => [],
|
||||
],
|
||||
'fcrm_pmpro_is_in_membership' => [
|
||||
'source' => ['pmpro', 'in_membership'],
|
||||
'operator' => 'in',
|
||||
'value_access' => 'level_ids',
|
||||
'default_value' => [],
|
||||
],
|
||||
'fcrm_lifter_is_in_course' => [
|
||||
'source' => ['lifterlms', 'purchased_items'],
|
||||
'operator' => 'in',
|
||||
'value_access' => 'course_ids',
|
||||
'default_value' => [],
|
||||
],
|
||||
'fcrm_lifter_is_in_membership' => [
|
||||
'source' => ['lifterlms', 'purchased_groups'],
|
||||
'operator' => 'in',
|
||||
'value_access' => 'course_ids',
|
||||
'default_value' => [],
|
||||
],
|
||||
'fcrm_learndhash_is_in_course' => [
|
||||
'source' => ['learndash', 'purchased_items'],
|
||||
'operator' => 'in',
|
||||
'value_access' => 'course_ids',
|
||||
'default_value' => [],
|
||||
],
|
||||
'fcrm_learndhash_is_in_group' => [
|
||||
'source' => ['learndash', 'purchased_groups'],
|
||||
'operator' => 'in',
|
||||
'value_access' => 'group_ids',
|
||||
'default_value' => [],
|
||||
],
|
||||
'fcrm_edd_is_purchased' => [
|
||||
'source' => ['edd', 'purchased_items'],
|
||||
'operator' => 'in',
|
||||
'value_access' => 'product_ids',
|
||||
'default_value' => [],
|
||||
],
|
||||
'fcrm_rcp_is_in_membership' => [
|
||||
'source' => ['rcp', 'in_membership'],
|
||||
'operator' => 'in',
|
||||
'value_access' => 'level_ids',
|
||||
'default_value' => [],
|
||||
]
|
||||
];
|
||||
|
||||
$operatorMaps = [
|
||||
'match_all' => 'in_all',
|
||||
'match_none_of' => 'not_in_all',
|
||||
'contains' => 'contains',
|
||||
'doNotContains' => 'not_contains',
|
||||
'startsWith' => 'startsWith',
|
||||
'endsWith' => 'endsWith'
|
||||
];
|
||||
|
||||
$conditionName = $sequence->action_name;
|
||||
|
||||
$oldSettings = $sequence->settings;
|
||||
|
||||
if (isset($simpleMaps[$conditionName])) {
|
||||
$map = $simpleMaps[$conditionName];
|
||||
$sequence->action_name = 'funnel_condition';
|
||||
$sequence->settings = [
|
||||
'conditions' => [
|
||||
[
|
||||
[
|
||||
'source' => $map['source'],
|
||||
'operator' => $map['operator'],
|
||||
'value' => Arr::get($sequence->settings, $map['value_access'], $map['default_value'])
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
if (!$dryRun) {
|
||||
$sequence->save();
|
||||
}
|
||||
} else if ($conditionName == 'fcrm_check_user_prop') {
|
||||
$sequence->action_name = 'funnel_condition';
|
||||
$conditionGroups = $oldSettings['condition_groups'];
|
||||
|
||||
$formattedConditions = [[]];
|
||||
|
||||
if (isset($conditionGroups[0])) {
|
||||
$conditions = $conditionGroups[0]['conditions'];
|
||||
$conditionType = $conditionGroups[0]['match_type'];
|
||||
|
||||
if ($conditionType != 'match_all') {
|
||||
$formattedConditions = [];
|
||||
}
|
||||
|
||||
foreach ($conditions as $condition) {
|
||||
$dataKey = $condition['data_key'];
|
||||
$operator = $condition['operator'];
|
||||
$dataValue = $condition['data_value'];
|
||||
if (!$dataKey || !$operator) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($operatorMaps[$operator])) {
|
||||
$operator = $operatorMaps[$operator];
|
||||
}
|
||||
|
||||
if ($dataKey == 'contact_type' || $dataKey == 'country') {
|
||||
if ($operator == '=') {
|
||||
$operator = 'in';
|
||||
} else {
|
||||
$operator = 'not_in';
|
||||
}
|
||||
if ($dataKey == 'country') {
|
||||
$dataValue = (array)$dataValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$provider = 'subscriber';
|
||||
|
||||
if (strpos($dataKey, 'custom.') === 0) {
|
||||
$provider = 'custom_fields';
|
||||
$dataKey = str_replace('custom.', '', $dataKey);
|
||||
}
|
||||
|
||||
$item = [
|
||||
'source' => [
|
||||
$provider,
|
||||
$dataKey
|
||||
],
|
||||
'operator' => $operator,
|
||||
'value' => $dataValue
|
||||
];
|
||||
|
||||
if ($conditionType == 'match_all') {
|
||||
$formattedConditions[0][] = $item;
|
||||
} else {
|
||||
$formattedConditions[] = [$item];
|
||||
}
|
||||
}
|
||||
}
|
||||
$sequence->settings = [
|
||||
'conditions' => $formattedConditions
|
||||
];
|
||||
if (!$dryRun) {
|
||||
$sequence->save();
|
||||
}
|
||||
} else if ($conditionName == 'fcrm_woo_conditions') {
|
||||
$sequence->action_name = 'funnel_condition';
|
||||
$conditionGroups = $sequence->settings['conditional_groups'];
|
||||
|
||||
$formattedConditions = [[]];
|
||||
|
||||
if (isset($conditionGroups[0])) {
|
||||
$conditions = $conditionGroups[0]['conditions'];
|
||||
|
||||
$keyMaps = [
|
||||
'order_total_value' => [
|
||||
'woo_order',
|
||||
'total_value'
|
||||
],
|
||||
'order_product_ids' => [
|
||||
'woo_order',
|
||||
'product_ids'
|
||||
],
|
||||
'order_cat_purchased' => [
|
||||
'woo_order',
|
||||
'cat_purchased'
|
||||
],
|
||||
'order_billing_country' => [
|
||||
'woo_order',
|
||||
'billing_country'
|
||||
],
|
||||
'order_shipping_method' => [
|
||||
'woo_order',
|
||||
'shipping_method'
|
||||
],
|
||||
'order_payment_gateway' => [
|
||||
'woo_order',
|
||||
'payment_gateway'
|
||||
],
|
||||
|
||||
'customer_total_spend' => [
|
||||
'woo',
|
||||
'total_order_value'
|
||||
],
|
||||
'customer_order_count' => [
|
||||
'woo',
|
||||
'total_spend'
|
||||
],
|
||||
'customer_guest_user' => [
|
||||
'woo',
|
||||
'guest_user'
|
||||
],
|
||||
'customer_billing_country' => [
|
||||
'woo',
|
||||
'billing_country'
|
||||
],
|
||||
'customer_cat_purchased' => [
|
||||
'woo',
|
||||
'purchased_categories'
|
||||
],
|
||||
'customer_purchased_products' => [
|
||||
'woo',
|
||||
'purchased_items'
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($conditions as $condition) {
|
||||
$dataKey = $condition['data_key'];
|
||||
$operator = $condition['operator'];
|
||||
$dataValue = $condition['data_value'];
|
||||
|
||||
if (!$dataKey || !$operator || !isset($keyMaps[$dataKey])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($operatorMaps[$operator])) {
|
||||
$operator = $operatorMaps[$operator];
|
||||
}
|
||||
|
||||
if ($dataKey == 'order_billing_country' || $dataKey == 'customer_billing_country') {
|
||||
if ($operator != '=') {
|
||||
$operator = 'not_in';
|
||||
} else {
|
||||
$operator = 'in';
|
||||
}
|
||||
|
||||
$dataValue = (array)$dataValue;
|
||||
}
|
||||
|
||||
$item = [
|
||||
'source' => $keyMaps[$dataKey],
|
||||
'operator' => $operator,
|
||||
'value' => $dataValue
|
||||
];
|
||||
$formattedConditions[0][] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$sequence->settings = [
|
||||
'conditions' => $formattedConditions
|
||||
];
|
||||
|
||||
if (!$dryRun) {
|
||||
$sequence->save();
|
||||
}
|
||||
}
|
||||
|
||||
return $sequence;
|
||||
}
|
||||
|
||||
|
||||
public static function createWpUserFromSubscriber($subscriber, $sendWelcomeEmail = false, $password = '', $role = '', $metaData = [])
|
||||
{
|
||||
if ($userId = $subscriber->getWpUserId()) {
|
||||
return $userId;
|
||||
}
|
||||
|
||||
if (!$password) {
|
||||
$password = wp_generate_password(8);
|
||||
}
|
||||
|
||||
$userId = wp_create_user(sanitize_user($subscriber->email), $password, $subscriber->email);
|
||||
if (is_wp_error($userId)) {
|
||||
return $userId;
|
||||
}
|
||||
|
||||
if (!$role) {
|
||||
// get default user role of WordPress
|
||||
$role = get_option('default_role');
|
||||
if (!$role) {
|
||||
$role = 'subscriber';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$user = new \WP_User($userId);
|
||||
$user->set_role($role);
|
||||
|
||||
$metaData['first_name'] = $subscriber->first_name;
|
||||
$metaData['last_name'] = $subscriber->last_name;
|
||||
|
||||
$userMetas = array_filter($metaData);
|
||||
|
||||
foreach ($userMetas as $metaKey => $metaValue) {
|
||||
update_user_meta($userId, $metaKey, $metaValue);
|
||||
}
|
||||
|
||||
if ($sendWelcomeEmail) {
|
||||
wp_send_new_user_notifications($userId, 'user');
|
||||
}
|
||||
|
||||
$subscriber->user_id = $userId;
|
||||
$subscriber->save();
|
||||
return $userId;
|
||||
}
|
||||
|
||||
|
||||
public static function getCountryShortName($countryName)
|
||||
{
|
||||
if (!function_exists('getFluentFormCountryList')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$countries = getFluentFormCountryList();
|
||||
if (isset($countries[strtoupper($countryName)])) {
|
||||
return $countryName;
|
||||
}
|
||||
|
||||
$countries = array_flip($countries);
|
||||
if (isset($countries[$countryName])) {
|
||||
return $countries[$countryName];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function getFunnelSubscriberStatus($defaultStatus, $funnel, $subscriber)
|
||||
{
|
||||
if ($defaultStatus == 'active' || $defaultStatus == 'waiting') {
|
||||
return $defaultStatus;
|
||||
}
|
||||
|
||||
$processableStatuses = ['subscribed', 'transactional'];
|
||||
if (in_array($subscriber->status, $processableStatuses, true)) {
|
||||
return 'active';
|
||||
}
|
||||
|
||||
if (Arr::get($funnel->settings, '__force_run_actions') == 'yes') {
|
||||
return 'active';
|
||||
}
|
||||
|
||||
return $defaultStatus;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,562 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel;
|
||||
|
||||
use FluentCrm\App\Models\Funnel;
|
||||
use FluentCrm\App\Models\FunnelMetric;
|
||||
use FluentCrm\App\Models\FunnelSequence;
|
||||
use FluentCrm\App\Models\FunnelSubscriber;
|
||||
use FluentCrm\App\Models\Subscriber;
|
||||
use FluentCrm\App\Services\Helper;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
class FunnelProcessor
|
||||
{
|
||||
private $subscribersCache = [];
|
||||
|
||||
private $sequenceFunnelCache = [];
|
||||
|
||||
private $funnelCache = [];
|
||||
|
||||
public function getSubscriber($id)
|
||||
{
|
||||
if (isset($this->subscribersCache[$id])) {
|
||||
return $this->subscribersCache[$id];
|
||||
}
|
||||
$subscriber = Subscriber::find($id);
|
||||
$this->subscribersCache[$id] = $subscriber;
|
||||
return $this->subscribersCache[$id];
|
||||
}
|
||||
|
||||
public function setSubscriber($id)
|
||||
{
|
||||
$subscriber = Subscriber::find($id);
|
||||
$this->subscribersCache[$id] = $subscriber;
|
||||
return $this->subscribersCache[$id];
|
||||
}
|
||||
|
||||
public function getSequence($id)
|
||||
{
|
||||
if (isset($this->sequenceFunnelCache[$id])) {
|
||||
return $this->sequenceFunnelCache[$id];
|
||||
}
|
||||
|
||||
$funnelAction = FunnelSequence::find($id);
|
||||
$this->sequenceFunnelCache[$id] = $funnelAction;
|
||||
return $this->sequenceFunnelCache[$id];
|
||||
}
|
||||
|
||||
public function getFunnel($id)
|
||||
{
|
||||
if (isset($this->funnelCache[$id])) {
|
||||
return $this->funnelCache[$id];
|
||||
}
|
||||
$funnel = Funnel::find($id);
|
||||
$this->funnelCache[$id] = $funnel;
|
||||
return $this->funnelCache[$id];
|
||||
}
|
||||
|
||||
public function setFunnel($id)
|
||||
{
|
||||
$funnel = Funnel::find($id);
|
||||
$this->funnelCache[$id] = $funnel;
|
||||
return $this->funnelCache[$id];
|
||||
}
|
||||
|
||||
public function startFunnelSequence($funnel, $subscriberData, $funnelSubArgs = [], $subscriber = false)
|
||||
{
|
||||
if (isset($subscriberData['email'])) {
|
||||
$subscriber = Subscriber::where('email', $subscriberData['email'])->first();
|
||||
}
|
||||
|
||||
if (!$subscriber) {
|
||||
// it's new so let's create new subscriber
|
||||
$subscriber = FunnelHelper::createOrUpdateContact($subscriberData);
|
||||
|
||||
if (!$subscriber) {
|
||||
return false;
|
||||
}
|
||||
} elseif (Arr::get($subscriberData, 'status') === 'pending' && $subscriber->status !== 'subscribed') {
|
||||
// update status only if it's not subscribed already
|
||||
$subscriber->status = 'pending';
|
||||
$subscriber->save();
|
||||
}
|
||||
|
||||
if ($subscriber->status == 'pending') {
|
||||
$subscriber->sendDoubleOptinEmail();
|
||||
}
|
||||
|
||||
$args = [
|
||||
'status' => ($subscriber->status == 'pending' || $subscriber->status == 'unsubscribed') ? 'pending' : 'draft'
|
||||
];
|
||||
|
||||
if ($funnelSubArgs) {
|
||||
$args = wp_parse_args($args, $funnelSubArgs);
|
||||
}
|
||||
|
||||
$this->startSequences($subscriber, $funnel, $args);
|
||||
}
|
||||
|
||||
public function startSequences($subscriber, $funnel, $funnelSubArgs = [])
|
||||
{
|
||||
// Check if already in funnel to prevent duplicate entries
|
||||
$existing = FunnelHelper::ifAlreadyInFunnel($funnel->id, $subscriber->id);
|
||||
if ($existing) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'funnel_id' => $funnel->id,
|
||||
'subscriber_id' => $subscriber->id,
|
||||
'status' => 'draft'
|
||||
];
|
||||
|
||||
if ($funnelSubArgs) {
|
||||
$data = wp_parse_args($funnelSubArgs, $data);
|
||||
}
|
||||
|
||||
$data['status'] = FunnelHelper::getFunnelSubscriberStatus($data['status'], $funnel, $subscriber);
|
||||
|
||||
// Unique index on (funnel_id, subscriber_id) prevents duplicates at DB level
|
||||
try {
|
||||
$funnelSubscriber = FunnelSubscriber::create($data);
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sequencePoints = (new SequencePoints($funnel, $funnelSubscriber));
|
||||
|
||||
if (!$sequencePoints->hasSequences()) {
|
||||
FunnelSubscriber::where('id', $funnelSubscriber->id)->delete();
|
||||
return;
|
||||
}
|
||||
|
||||
do_action('fluent_crm/automation_funnel_start', $funnel, $subscriber);
|
||||
|
||||
if ($funnelSubscriber->status != 'pending') {
|
||||
$this->processSequencePoints($sequencePoints, $subscriber, $funnelSubscriber);
|
||||
}
|
||||
}
|
||||
|
||||
public function processSequencePoints(SequencePoints $sequencePoints, $subscriber, $funnelSubscriber)
|
||||
{
|
||||
if (!$sequencePoints->hasSequences()) {
|
||||
$this->completeFunnelSequence($funnelSubscriber);
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute immediate sequences first (they may include conditionals
|
||||
// that set their own next state via initChildSequences)
|
||||
$hasConditional = false;
|
||||
foreach ($sequencePoints->getCurrentSequences() as $sequence) {
|
||||
if ($sequence->type == 'conditional') {
|
||||
$hasConditional = true;
|
||||
}
|
||||
$this->processSequence($subscriber, $sequence, $funnelSubscriber->id);
|
||||
if ($sequence->action_name == 'end_this_funnel') {
|
||||
$this->completeFunnelSequence($funnelSubscriber);
|
||||
$this->setFunnel($funnelSubscriber->funnel_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If a conditional was processed, initChildSequences() already
|
||||
// handled the subscriber's next state — skip status update
|
||||
if ($hasConditional) {
|
||||
return;
|
||||
}
|
||||
|
||||
$nextSequence = $sequencePoints->getNextSequence();
|
||||
$requiredBenchMark = $sequencePoints->getRequiredBenchmark();
|
||||
|
||||
if ($nextSequence && $requiredBenchMark) {
|
||||
if ($nextSequence->sequence < $requiredBenchMark->sequence) {
|
||||
$requiredBenchMark = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($requiredBenchMark) {
|
||||
FunnelSubscriber::where('id', $funnelSubscriber->id)
|
||||
->update([
|
||||
'next_sequence' => $requiredBenchMark->sequence,
|
||||
'next_sequence_id' => $requiredBenchMark->id,
|
||||
'next_execution_time' => NULL,
|
||||
'status' => 'waiting'
|
||||
]);
|
||||
} else if (!$sequencePoints->hasNext()) {
|
||||
$this->completeFunnelSequence($funnelSubscriber);
|
||||
} else if ($nextSequence) {
|
||||
$nextDateTime = gmdate('Y-m-d H:i:s', current_time('timestamp') + $nextSequence->delay);
|
||||
|
||||
if ($nextSequence->execution_date_time) {
|
||||
$nextDateTime = $nextSequence->execution_date_time;
|
||||
}
|
||||
|
||||
FunnelSubscriber::where('id', $funnelSubscriber->id)
|
||||
->update([
|
||||
'next_sequence' => $nextSequence->sequence,
|
||||
'next_sequence_id' => $nextSequence->id,
|
||||
'next_execution_time' => $nextDateTime,
|
||||
'status' => 'active'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function processSequence($subscriber, $sequence, $funnelSubscriberId)
|
||||
{
|
||||
$funnelMetric = $this->recordFunnelMetric($subscriber, $sequence);
|
||||
|
||||
// Mark complete BEFORE execution by design: prevents double-execution if the process
|
||||
// crashes mid-action. The FunnelMetric + wasRecentlyCreated check ensures each action
|
||||
// runs at most once per subscriber per sequence. Failed actions are not retried.
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $sequence->id, 'complete');
|
||||
|
||||
if ($sequence->type == 'conditional' && $sequence->action_name != 'funnel_condition') {
|
||||
$sequence = FunnelHelper::migrateConditionSequence($sequence);
|
||||
}
|
||||
|
||||
if ($funnelMetric->wasRecentlyCreated) {
|
||||
try {
|
||||
do_action('fluentcrm_funnel_sequence_handle_' . $sequence->action_name, $subscriber, $sequence, $funnelSubscriberId, $funnelMetric, $this);
|
||||
} catch (\Throwable $e) {
|
||||
Helper::debugLog('Funnel sequence error for ' . $sequence->funnel_id . ' => Funnel Sub ID: ' . $funnelSubscriberId, $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function resumeFunnelSubscriber($funnel, $subscriber, $funnelSubscriber)
|
||||
{
|
||||
$sequencePoints = new SequencePoints($funnel, $funnelSubscriber);
|
||||
$this->processSequencePoints($sequencePoints, $subscriber, $funnelSubscriber);
|
||||
}
|
||||
|
||||
public function completeFunnelSequence($funnelSubscriber)
|
||||
{
|
||||
FunnelSubscriber::where('id', $funnelSubscriber->id)
|
||||
->update([
|
||||
'status' => 'completed'
|
||||
]);
|
||||
|
||||
$funnel = $this->getFunnel($funnelSubscriber->funnel_id);
|
||||
$subscriber = $this->getSubscriber($funnelSubscriber->subscriber_id);
|
||||
|
||||
if ($funnel && $subscriber) {
|
||||
do_action('fluent_crm/automation_funnel_completed', $funnel, $subscriber);
|
||||
}
|
||||
}
|
||||
|
||||
public function followUpSequenceActions()
|
||||
{
|
||||
update_option('_fc_last_funnel_processor_ran', time(), 'no');
|
||||
|
||||
/**
|
||||
* Apply a filter to retrieve the subscriber statuses for the funnel.
|
||||
*
|
||||
* This filter allows customization of the subscriber statuses used in the funnel processing.
|
||||
* By default, it includes only the 'active' status.
|
||||
*
|
||||
* @param array $statuses The default subscriber statuses, which is ['active'].
|
||||
* @return array Filtered subscriber statuses.
|
||||
* @since 1.0.0
|
||||
*
|
||||
*/
|
||||
$statuses = apply_filters('fluent_crm/funnel_subscriber_statuses', ['active']);
|
||||
|
||||
/* Funnel processor batch limit
|
||||
* This is the maximum number of funnel subscribers that will be processed in a single run.
|
||||
* By default, it is set to 200. FluentCRM wants to process 200 funnel subscribers each time
|
||||
* @param int $batchLimit The batch limit for the funnel processor.
|
||||
* @return int Filtered batch limit.
|
||||
* @since 3.0.0
|
||||
*/
|
||||
$batchLimit = (int)apply_filters('fluent_crm/funnel_processor_batch_limit', 200);
|
||||
if ($batchLimit < 1) {
|
||||
$batchLimit = 1;
|
||||
}
|
||||
|
||||
$jobs = FunnelSubscriber::join('fc_funnels', function ($join) {
|
||||
$join->on('fc_funnel_subscribers.funnel_id', '=', 'fc_funnels.id')
|
||||
->where('fc_funnels.status', '=', 'published')
|
||||
->where('fc_funnels.type', '=', 'funnels');
|
||||
})
|
||||
->whereIn('fc_funnel_subscribers.status', $statuses)
|
||||
->where('fc_funnel_subscribers.next_execution_time', '<=', current_time('mysql'))
|
||||
->whereNotNull('fc_funnel_subscribers.next_execution_time')
|
||||
->orderBy('fc_funnel_subscribers.next_execution_time', 'ASC')
|
||||
->limit($batchLimit)
|
||||
->select('fc_funnel_subscribers.*')
|
||||
->get();
|
||||
|
||||
$startingAt = time();
|
||||
|
||||
$completed = 0;
|
||||
|
||||
/* Funnel processor max processing seconds
|
||||
* This is the maximum number of seconds that the funnel processor will run for.
|
||||
* By default, it is set to 55 seconds.
|
||||
* This is a safety mechanism to prevent the funnel processor from running for too long
|
||||
* @since 3.0.0
|
||||
*/
|
||||
$maxProcessingSeconds = (int)apply_filters('fluent_crm/funnel_processor_max_processing_seconds', 55);
|
||||
if ($maxProcessingSeconds < 1) {
|
||||
$maxProcessingSeconds = 1;
|
||||
}
|
||||
|
||||
foreach ($jobs as $job) {
|
||||
if ((time() - $startingAt) > $maxProcessingSeconds) {
|
||||
break;
|
||||
}
|
||||
|
||||
$completed++;
|
||||
|
||||
$this->processFunnelAction($job);
|
||||
}
|
||||
|
||||
Helper::debugLog('Automation followUpSequenceActions', 'Completed Jobs Count: ' . $completed);
|
||||
}
|
||||
|
||||
public function processFunnelAction($funnelSubscriber)
|
||||
{
|
||||
$subscriber = $this->getSubscriber($funnelSubscriber->subscriber_id);
|
||||
$funnel = $this->getFunnel($funnelSubscriber->funnel_id);
|
||||
|
||||
if (!$subscriber || !$funnel) {
|
||||
FunnelSubscriber::where('id', $funnelSubscriber->id)->update([
|
||||
'status' => 'skipped'
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!in_array($subscriber->status, ['subscribed', 'transactional'])) {
|
||||
$forceRun = Arr::get($funnel->settings, '__force_run_actions') === 'yes';
|
||||
if (!$forceRun) {
|
||||
FunnelSubscriber::where('id', $funnelSubscriber->id)->update([
|
||||
'status' => 'cancelled'
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$sequencePoints = new SequencePoints($funnel, $funnelSubscriber);
|
||||
|
||||
$this->processSequencePoints($sequencePoints, $subscriber, $funnelSubscriber);
|
||||
}
|
||||
|
||||
public function startFunnelFromSequencePoint($startSequence, $subscriber, $args = [], $metricArgs = [])
|
||||
{
|
||||
if (!$subscriber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$funnelSubscriber = FunnelHelper::ifAlreadyInFunnel($startSequence->funnel_id, $subscriber->id);
|
||||
|
||||
if (!$funnelSubscriber && $startSequence->type == 'benchmark') {
|
||||
// it's new starting point for a goal type sequence
|
||||
// so if the can start is set to no then we will skip this
|
||||
if (Arr::get($startSequence->settings, 'can_enter') == 'no') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($funnelSubscriber && ($funnelSubscriber->status == 'completed' || $funnelSubscriber->status == 'cancelled')) {
|
||||
return false; // It's already completed or cancelled. We don't need to start again
|
||||
}
|
||||
|
||||
|
||||
$this->recordFunnelMetric($subscriber, $startSequence, $metricArgs);
|
||||
|
||||
if (!$funnelSubscriber) {
|
||||
|
||||
$processableStatuses = ['subscribed', 'transactional'];
|
||||
|
||||
// we have to create a funnel subscriber
|
||||
$data = [
|
||||
'funnel_id' => $startSequence->funnel_id,
|
||||
'subscriber_id' => $subscriber->id,
|
||||
'status' => (in_array($subscriber->status, $processableStatuses, true)) ? 'active' : 'pending',
|
||||
'starting_sequence_id' => $startSequence->id,
|
||||
'last_sequence_status' => 'completed',
|
||||
'next_sequence' => $startSequence->sequence + 1,
|
||||
'last_sequence_id' => $startSequence->id,
|
||||
'last_executed_time' => current_time('mysql'),
|
||||
'source_trigger_name' => $startSequence->action_name
|
||||
];
|
||||
|
||||
if ($args) {
|
||||
$data = wp_parse_args($args, $data);
|
||||
}
|
||||
|
||||
if ($data['status'] != 'active') {
|
||||
$data['status'] = FunnelHelper::getFunnelSubscriberStatus($data['status'], $this->getFunnel($startSequence->funnel_id), $subscriber);
|
||||
}
|
||||
|
||||
// Unique index on (funnel_id, subscriber_id) prevents duplicates at DB level
|
||||
try {
|
||||
$funnelSubscriber = FunnelSubscriber::create($data);
|
||||
} catch (\Exception $e) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// We already have funnel subscriber. Now we have to update that
|
||||
$lastSequence = $funnelSubscriber->last_sequence;
|
||||
|
||||
if (!$lastSequence || ($lastSequence->sequence <= $startSequence->sequence)) {
|
||||
$nextSequence = FunnelSequence::where('sequence', '>', $startSequence->sequence)
|
||||
->where('funnel_id', $startSequence->funnel_id)
|
||||
->orderBy('sequence', 'ASC')
|
||||
->first();
|
||||
|
||||
if (!$nextSequence) {
|
||||
$this->completeFunnelSequence($funnelSubscriber);
|
||||
return;
|
||||
}
|
||||
|
||||
$funnelSubscriber->last_sequence_id = $startSequence->id;
|
||||
$funnelSubscriber->next_sequence_id = $nextSequence->id;
|
||||
$funnelSubscriber->next_sequence = $nextSequence->sequence;
|
||||
if ($funnelSubscriber->status == 'waiting') {
|
||||
$funnelSubscriber->status = 'active';
|
||||
}
|
||||
$funnelSubscriber->next_execution_time = current_time('mysql'); // we are auto advancing the funnel
|
||||
$funnelSubscriber->save();
|
||||
} else {
|
||||
// this already advanced than our target
|
||||
// We have to check if we have to fire immediately
|
||||
if ($funnelSubscriber->next_sequence - 1 == $startSequence->sequence) {
|
||||
// we are just make the time with current time if that had a timer for the target sequence
|
||||
$funnelSubscriber->next_execution_time = current_time('mysql');
|
||||
$funnelSubscriber->save();
|
||||
} else {
|
||||
return; // It will work as it is; This funnel don't need any help
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($funnelSubscriber->status == 'pending') {
|
||||
return; // We need double-optin from this user.
|
||||
}
|
||||
|
||||
$funnel = $this->getFunnel($startSequence->funnel_id);
|
||||
|
||||
$sequencePoints = new SequencePoints($funnel, $funnelSubscriber);
|
||||
|
||||
$this->processSequencePoints($sequencePoints, $subscriber, $funnelSubscriber);
|
||||
}
|
||||
|
||||
public function recordFunnelMetric($subscriber, $sequence, $metricArgs = [])
|
||||
{
|
||||
$lookupData = [
|
||||
'funnel_id' => $sequence->funnel_id,
|
||||
'sequence_id' => $sequence->id,
|
||||
'subscriber_id' => $subscriber->id
|
||||
];
|
||||
|
||||
$extraData = $metricArgs ?: [];
|
||||
|
||||
try {
|
||||
return FunnelMetric::firstOrCreate($lookupData, $extraData);
|
||||
} catch (\Exception $e) {
|
||||
// Race condition: another process inserted between the SELECT and INSERT
|
||||
$existing = FunnelMetric::where($lookupData)->first();
|
||||
|
||||
if ($existing) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function initChildSequences($parent, $isMatched, $subscriber, $funnelSubscriberId, $funnelMetric)
|
||||
{
|
||||
$conditionType = 'no';
|
||||
if ($isMatched) {
|
||||
$conditionType = 'yes';
|
||||
}
|
||||
// find the corresponding sequence
|
||||
$sequences = FunnelSequence::where('funnel_id', $parent->funnel_id)
|
||||
->where('parent_id', $parent->id)
|
||||
->orderBy('sequence', 'ASC')
|
||||
->where('condition_type', $conditionType)
|
||||
->get();
|
||||
|
||||
$waitTimes = 0;
|
||||
if (!$sequences->isEmpty()) {
|
||||
$immediateSequences = [];
|
||||
$firstSequence = $sequences[0];
|
||||
$nextSequence = false;
|
||||
|
||||
foreach ($sequences as $sequence) {
|
||||
if ($sequence->c_delay == $firstSequence->c_delay) {
|
||||
$immediateSequences[] = $sequence;
|
||||
} else {
|
||||
if (!$nextSequence) {
|
||||
$nextSequence = $sequence;
|
||||
}
|
||||
if ($sequence->c_delay < $nextSequence->c_delay) {
|
||||
$nextSequence = $sequence;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($immediateSequences as $immediateSequence) {
|
||||
$this->processSequence($subscriber, $immediateSequence, $funnelSubscriberId);
|
||||
if ($immediateSequence->action_name == 'end_this_funnel') {
|
||||
$funnelSub = FunnelSubscriber::where('id', $funnelSubscriberId)->first();
|
||||
if ($funnelSub) {
|
||||
$this->completeFunnelSequence($funnelSub);
|
||||
}
|
||||
$this->setFunnel($immediateSequence->funnel_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($immediateSequence->action_name == 'fluentcrm_wait_times') {
|
||||
$waitTimes = FunnelHelper::getCurrentDelayInSeconds($immediateSequence->settings, $immediateSequence, $funnelSubscriberId);
|
||||
}
|
||||
}
|
||||
|
||||
if ($nextSequence) {
|
||||
|
||||
$waitDateTimes = gmdate('Y-m-d H:i:s', current_time('timestamp') + $nextSequence->delay);
|
||||
|
||||
if ($waitTimes) {
|
||||
$waitDateTimes = gmdate('Y-m-d H:i:s', current_time('timestamp') + $waitTimes);
|
||||
}
|
||||
|
||||
return FunnelSubscriber::where('id', $funnelSubscriberId)
|
||||
->update([
|
||||
'next_sequence' => $nextSequence->sequence,
|
||||
'next_sequence_id' => $nextSequence->id,
|
||||
'next_execution_time' => $waitDateTimes,
|
||||
'status' => 'active'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$funnelSubscriber = FunnelSubscriber::where('id', $funnelSubscriberId)->first();
|
||||
if (!$funnelSubscriber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$funnelSubscriber->last_sequence_id = $parent->id;
|
||||
FunnelHelper::changeFunnelSubSequenceStatus($funnelSubscriberId, $parent->id);
|
||||
$funnel = $this->getFunnel($parent->funnel_id);
|
||||
|
||||
// we don't have next sequence so we have to loop back to the parent
|
||||
$sequencePoints = new SequencePoints($funnel, $funnelSubscriber);
|
||||
|
||||
if ($waitTimes && $currentNextSequences = $sequencePoints->getCurrentSequences()) {
|
||||
$nextSequence = $currentNextSequences[0];
|
||||
return FunnelSubscriber::where('id', $funnelSubscriberId)
|
||||
->update([
|
||||
'next_sequence' => $nextSequence->sequence,
|
||||
'next_sequence_id' => $nextSequence->id,
|
||||
'next_execution_time' => gmdate('Y-m-d H:i:s', current_time('timestamp') + $waitTimes),
|
||||
'status' => 'active'
|
||||
]);
|
||||
}
|
||||
|
||||
$this->processSequencePoints($sequencePoints, $subscriber, $funnelSubscriber);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,674 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel;
|
||||
|
||||
use FluentCrm\App\Services\Helper;
|
||||
|
||||
class ProFunnelItems
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
add_filter('fluentcrm_funnel_triggers', function ($allTriggers) {
|
||||
$triggers = $this->getProTriggers();
|
||||
return array_merge($allTriggers, $triggers);
|
||||
});
|
||||
|
||||
add_filter('fluentcrm_funnel_blocks', function ($funnelBlocks) {
|
||||
$blocks = $this->getProBlocks();
|
||||
return array_merge($funnelBlocks, $blocks);
|
||||
}, 100);
|
||||
|
||||
}
|
||||
|
||||
private function getProTriggers()
|
||||
{
|
||||
$triggers = $this->getCrmTriggers();
|
||||
if (defined('WC_PLUGIN_FILE')) {
|
||||
$triggers['woocommerce_order_status_processing'] = [
|
||||
'category' => 'WooCommerce',
|
||||
'label' => 'New Order (Processing)',
|
||||
'icon' => 'fc-icon-woo_new_order',
|
||||
'description' => 'This funnel will start once a new order will be added as processing',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['woocommerce_order_status_completed'] = [
|
||||
'category' => 'WooCommerce',
|
||||
'label' => 'Order Completed',
|
||||
'icon' => 'fc-icon-woo_order_complete',
|
||||
'description' => 'This funnel will start when an order is completed',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['woocommerce_order_status_refunded'] = [
|
||||
'category' => 'WooCommerce',
|
||||
'label' => 'Order Refunded',
|
||||
'icon' => 'fc-icon-woo_refund',
|
||||
'description' => 'This funnel will start when an order is refunded',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['woocommerce_order_status_changed'] = [
|
||||
'category' => 'WooCommerce',
|
||||
'label' => 'Order Status Changed',
|
||||
'icon' => 'fc-icon-woo',
|
||||
'description' => 'This funnel will start when an order status changes',
|
||||
'disabled' => true
|
||||
];
|
||||
}
|
||||
|
||||
if (defined('WCS_INIT_TIMESTAMP')) {
|
||||
$triggers['woocommerce_subscription_status_active'] = [
|
||||
'category' => 'WooCommerce',
|
||||
'label' => 'Subscription activated',
|
||||
'icon' => 'fc-icon-woo_order_complete',
|
||||
'description' => 'This funnel will start when a WooCommerce subscription begins or its status changes to active.',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['woocommerce_subscription_renewal_payment_complete'] = [
|
||||
'category' => 'WooCommerce',
|
||||
'label' => 'Renewal Payment Received',
|
||||
'icon' => 'fc-icon-woo_order_complete',
|
||||
'description' => 'This funnel will start when a recurring payment received for a subscription',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['woocommerce_subscription_renewal_payment_failed'] = [
|
||||
'category' => 'WooCommerce',
|
||||
'label' => 'Renewal Payment Failed',
|
||||
'icon' => 'fc-icon-woo_refund',
|
||||
'description' => 'This funnel will start when a subscription payment fails',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['woocommerce_subscription_status_cancelled'] = [
|
||||
'category' => 'WooCommerce',
|
||||
'label' => 'WooCommerce Subscription Cancelled',
|
||||
'icon' => 'fc-icon-woo_refund',
|
||||
'description' => 'This funnel will start when a WooCommerce subscription is cancelled.',
|
||||
'disabled' => true
|
||||
];
|
||||
}
|
||||
|
||||
if (defined('WLM3_PLUGIN_VERSION')) {
|
||||
$triggers['wishlistmember_add_user_levels'] = [
|
||||
'category' => 'Wishlist Member',
|
||||
'label' => 'Membership Enrolled',
|
||||
'icon' => 'fc-icon-wishlist',
|
||||
'description' => 'This funnel runs when a member is added to a membership level',
|
||||
'disabled' => true
|
||||
];
|
||||
}
|
||||
|
||||
if (defined('TUTOR_VERSION')) {
|
||||
$triggers['tutor_after_enrolled'] = [
|
||||
'category' => 'TutorLMS',
|
||||
'label' => 'Course Enrolled',
|
||||
'icon' => 'fc-icon-tutor_lms_enrollment_course',
|
||||
'description' => 'This funnel runs when a student is enrolled in a course',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['tutor_course_complete_after'] = [
|
||||
'category' => 'TutorLMS',
|
||||
'label' => 'Course Completed',
|
||||
'icon' => 'fc-icon-tutor_lms_complete_course',
|
||||
'description' => 'This funnel runs when a student completes a course',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['tutor_lesson_completed_after'] = [
|
||||
'category' => 'TutorLMS',
|
||||
'label' => 'Lesson Completed',
|
||||
'icon' => 'fc-icon-tutor_lms_complete_course',
|
||||
'description' => 'This funnel runs when a student completes a lesson',
|
||||
'disabled' => true
|
||||
];
|
||||
}
|
||||
|
||||
if (class_exists('\Restrict_Content_Pro')) {
|
||||
$triggers['rcp_membership_post_activate'] = [
|
||||
'category' => 'Restrict Content Pro',
|
||||
'label' => 'Membership Enrolled',
|
||||
'icon' => 'fc-icon-rcp_membership_level',
|
||||
'description' => 'This funnel runs when a member is added to a membership level',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['rcp_transition_membership_status_expired'] = [
|
||||
'category' => 'Restrict Content Pro',
|
||||
'label' => 'Membership Expired',
|
||||
'icon' => 'fc-icon-rcp_membership_cancle',
|
||||
'description' => 'This funnel runs when a membership expires',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['rcp_membership_post_cancel'] = [
|
||||
'category' => 'Restrict Content Pro',
|
||||
'label' => 'Membership Cancelled',
|
||||
'icon' => 'fc-icon-rcp_membership_cancle',
|
||||
'description' => 'This funnel runs when a membership is cancelled',
|
||||
'disabled' => true
|
||||
];
|
||||
}
|
||||
|
||||
if (defined('PMPRO_VERSION')) {
|
||||
$triggers['pmpro_after_change_membership_level'] = [
|
||||
'category' => 'Paid Membership Pro',
|
||||
'label' => 'Membership Enrolled',
|
||||
'icon' => 'fc-icon-paid_membership_pro_user_level',
|
||||
'description' => 'This funnel runs when a member is added to a membership level',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['pmpro_membership_post_membership_expiry'] = [
|
||||
'category' => 'Paid Membership Pro',
|
||||
'label' => 'Membership Level Expired',
|
||||
'icon' => 'fc-icon-membership_level_ex_pmp',
|
||||
'description' => 'This funnel runs when a membership expires',
|
||||
'disabled' => true
|
||||
];
|
||||
}
|
||||
|
||||
if (defined('MEPR_PLUGIN_NAME')) {
|
||||
$triggers['mepr-account-is-active'] = [
|
||||
'category' => 'MemberPress',
|
||||
'label' => 'Membership Enrolled',
|
||||
'icon' => 'fc-icon-memberpress_membership',
|
||||
'description' => 'This funnel runs when a member is added to a membership level',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['mepr-event-transaction-expired'] = [
|
||||
'category' => 'MemberPress',
|
||||
'label' => 'Subscription expired',
|
||||
'icon' => 'el-icon-circle-close',
|
||||
'description' => 'This funnel runs when a subscription expires',
|
||||
'disabled' => true
|
||||
];
|
||||
}
|
||||
|
||||
if (defined('LLMS_PLUGIN_FILE')) {
|
||||
$triggers['llms_user_enrolled_in_course'] = [
|
||||
'category' => 'LifterLMS',
|
||||
'label' => 'Course Enrolled',
|
||||
'icon' => 'fc-icon-lifter_lms_course_enrollment',
|
||||
'description' => 'This funnel runs when a contact is enrolled in a course',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['lifterlms_course_completed'] = [
|
||||
'category' => 'LifterLMS',
|
||||
'label' => 'Course Completed',
|
||||
'icon' => 'fc-icon-lifter_lms_complete_course',
|
||||
'description' => 'This funnel runs when a student completes a course',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['llms_user_added_to_membership_level'] = [
|
||||
'category' => 'LifterLMS',
|
||||
'label' => 'Joined Membership',
|
||||
'icon' => 'fc-icon-lifter_lms_membership',
|
||||
'description' => 'This funnel runs when a student has been enrolled in a membership level',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['lifterlms_lesson_completed'] = [
|
||||
'category' => 'LifterLMS',
|
||||
'label' => 'Lesson Completed',
|
||||
'icon' => 'fc-icon-lifter_lms_complete_lession-t2',
|
||||
'description' => 'This funnel runs when a student completes a lesson',
|
||||
'disabled' => true
|
||||
];
|
||||
}
|
||||
|
||||
if (defined('LEARNDASH_VERSION')) {
|
||||
$triggers['learndash_update_course_access'] = [
|
||||
'category' => 'LearnDash',
|
||||
'label' => 'Course Enrolled',
|
||||
'icon' => 'fc-icon-learndash_enroll_course',
|
||||
'description' => 'This funnel runs when a student is enrolled in a course',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['learndash_lesson_completed'] = [
|
||||
'category' => 'LearnDash',
|
||||
'label' => 'Lesson Completed',
|
||||
'icon' => 'fc-icon-learndash_complete_lesson',
|
||||
'description' => 'This funnel runs a student completes a lesson',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['learndash_topic_completed'] = [
|
||||
'category' => 'LearnDash',
|
||||
'label' => 'Topic Completed',
|
||||
'icon' => 'fc-icon-learndash_complete_topic',
|
||||
'description' => 'This funnel runs when a student completes a lesson topic',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['learndash_course_completed'] = [
|
||||
'category' => 'LearnDash',
|
||||
'label' => 'Course Completed',
|
||||
'icon' => 'fc-icon-learndash_complete_course',
|
||||
'description' => 'This funnel runs when a student completes a course',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['ld_added_group_access'] = [
|
||||
'category' => 'LearnDash',
|
||||
'label' => 'Group Enrolled',
|
||||
'icon' => 'fc-icon-learndash_course_group',
|
||||
'description' => 'This funnel runs when a user is enrolled in a group',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['simulated_learndash_update_course_removed'] = [
|
||||
'category' => 'LearnDash',
|
||||
'label' => 'Course Left',
|
||||
'icon' => 'fc-icon-learndash_enroll_course',
|
||||
'description' => 'This funnel runs when a student leaves a course',
|
||||
'disabled' => true
|
||||
];
|
||||
}
|
||||
|
||||
if (Helper::isEdd3()) {
|
||||
$triggers['edd_update_payment_status'] = [
|
||||
'category' => 'Easy Digital Downloads',
|
||||
'label' => 'Edd - New Order Success',
|
||||
'icon' => 'fc-icon-edd_new_order_success',
|
||||
'description' => 'This funnel will start once new order payment is successful',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
if (defined('EDD_SL_VERSION')) {
|
||||
$triggers['edd_sl_post_set_status'] = [
|
||||
'category' => 'Easy Digital Downloads',
|
||||
'label' => 'License Expired',
|
||||
'description' => 'This funnel will start when a license gets expired',
|
||||
'disabled' => true
|
||||
];
|
||||
}
|
||||
|
||||
if (defined('EDD_RECURRING_VERSION')) {
|
||||
$triggers['edd_recurring_add_subscription_payment'] = [
|
||||
'category' => 'Easy Digital Downloads',
|
||||
'label' => 'Renewal Payment Received',
|
||||
'description' => 'This funnel will start when a renewal payment is received for an active subscription',
|
||||
'icon' => 'fc-icon-edd_new_order_success',
|
||||
'disabled' => true
|
||||
];
|
||||
|
||||
$triggers['edd_subscription_status_change'] = [
|
||||
'category' => 'Easy Digital Downloads',
|
||||
'label' => 'Recurring Subscription Expired',
|
||||
'description' => 'This funnel will start when a recurring subscription gets expired',
|
||||
'icon' => 'el-icon-circle-close',
|
||||
'disabled' => true
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (class_exists('\Affiliate_WP')) {
|
||||
$triggers['affwp_set_affiliate_status'] = [
|
||||
'category' => 'AffiliateWP',
|
||||
'label' => 'New Affiliate Joined',
|
||||
'icon' => 'fc-icon-trigger',
|
||||
'description' => 'This funnel will be initiated when a new affiliate gets approved/registered directly',
|
||||
'disabled' => true
|
||||
];
|
||||
}
|
||||
|
||||
if (defined('SURECART_PLUGIN_FILE')) {
|
||||
$triggers['fluent_surecart_purchase_created_wrap'] = [
|
||||
'category' => 'SureCart',
|
||||
'label' => 'SureCart - New Order Success',
|
||||
'icon' => 'el-icon-shopping-cart-full',
|
||||
'description' => 'This funnel will start when new order payment is successful',
|
||||
'disabled' => true,
|
||||
'svg' => '<svg width="16px" height="16px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 137 137" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M68.5 137C106.332 137 137 106.332 137 68.5C137 30.6685 106.332 0 68.5 0C30.6685 0 0 30.6685 0 68.5C0 106.332 30.6685 137 68.5 137ZM68.795 34.25C63.2947 34.25 55.6831 37.3955 51.7938 41.2756L41.2306 51.8141H93.8817L111.487 34.25H68.795ZM85.1174 95.7244C81.2282 99.6045 73.6165 102.75 68.1163 102.75H25.4242L43.0295 85.1859H95.6806L85.1174 95.7244ZM102.229 60.5962H32.4471L29.1508 63.8895C21.3458 70.9151 23.6606 76.4039 34.5912 76.4039H104.563L107.86 73.1106C115.589 66.1263 113.16 60.5962 102.229 60.5962Z" fill="#008156"/>
|
||||
<script xmlns=""/></svg>'
|
||||
];
|
||||
|
||||
$triggers['fluent_surecart_purchase_refund_wrap'] = [
|
||||
'category' => 'SureCart',
|
||||
'label' => 'SureCart - Order Revoked',
|
||||
'icon' => 'el-icon-sold-out',
|
||||
'description' => 'This funnel will start when order will be revoked',
|
||||
'disabled' => true,
|
||||
'svg' => '<svg width="16px" height="16px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 137 137" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M68.5 137C106.332 137 137 106.332 137 68.5C137 30.6685 106.332 0 68.5 0C30.6685 0 0 30.6685 0 68.5C0 106.332 30.6685 137 68.5 137ZM68.795 34.25C63.2947 34.25 55.6831 37.3955 51.7938 41.2756L41.2306 51.8141H93.8817L111.487 34.25H68.795ZM85.1174 95.7244C81.2282 99.6045 73.6165 102.75 68.1163 102.75H25.4242L43.0295 85.1859H95.6806L85.1174 95.7244ZM102.229 60.5962H32.4471L29.1508 63.8895C21.3458 70.9151 23.6606 76.4039 34.5912 76.4039H104.563L107.86 73.1106C115.589 66.1263 113.16 60.5962 102.229 60.5962Z" fill="#008156"/>
|
||||
<script xmlns=""/></svg>'
|
||||
];
|
||||
}
|
||||
|
||||
return $triggers;
|
||||
|
||||
}
|
||||
|
||||
private function getProBlocks()
|
||||
{
|
||||
$blocks = [
|
||||
'fcrm_has_contact_list' => [
|
||||
'is_pro' => true,
|
||||
'type' => 'conditional',
|
||||
'title' => 'Has In Selected Lists',
|
||||
'description' => 'Check If the contact has specific lists',
|
||||
'icon' => 'fc-icon-has_list',
|
||||
],
|
||||
'fcrm_has_contact_tag' => [
|
||||
'is_pro' => true,
|
||||
'type' => 'conditional',
|
||||
'title' => 'Has Selected tags',
|
||||
'description' => 'Check If the contact has specific tags',
|
||||
'icon' => 'fc-icon-has_list',
|
||||
],
|
||||
'fluencrm_benchmark_link_clicked' => [
|
||||
'is_pro' => true,
|
||||
'type' => 'benchmark',
|
||||
'title' => 'Link Click',
|
||||
'description' => 'This will run once a subscriber click on this provided link',
|
||||
'icon' => 'fc-icon-link_clicked',
|
||||
],
|
||||
'send_campaign_email' => [
|
||||
'is_pro' => true,
|
||||
'type' => 'action',
|
||||
'category' => 'Email',
|
||||
'title' => 'Send Campaign Email',
|
||||
'description' => 'Send an Email from your existing campaign',
|
||||
'icon' => 'fc-icon-send_campaign',
|
||||
],
|
||||
'remove_from_funnel' => [
|
||||
'is_pro' => true,
|
||||
'type' => 'action',
|
||||
'category' => 'CRM',
|
||||
'title' => 'Cancel Automations',
|
||||
'description' => 'Pause/Cancel another automation for contact',
|
||||
'icon' => 'fc-icon-cancel_automation',
|
||||
],
|
||||
'remove_from_email_sequence' => [
|
||||
'is_pro' => true,
|
||||
'type' => 'action',
|
||||
'category' => 'Email',
|
||||
'title' => 'Cancel Sequence Emails',
|
||||
'description' => 'Cancel Sequence Emails for the contact',
|
||||
'icon' => 'fc-icon-cancel_sequence',//fluentCrmMix('images/funnel_icons/cancel_sequence.svg')
|
||||
],
|
||||
'add_to_email_sequence' => [
|
||||
'is_pro' => true,
|
||||
'type' => 'action',
|
||||
'category' => 'Email',
|
||||
'title' => 'Set Sequence Emails',
|
||||
'description' => 'Send Automated Emails based on your Sequence settings',
|
||||
'icon' => 'fc-icon-set_sequence',
|
||||
],
|
||||
'update_contact_property' => [
|
||||
'is_pro' => true,
|
||||
'type' => 'action',
|
||||
'category' => 'CRM',
|
||||
'title' => 'Update Contact Property',
|
||||
'description' => 'Update custom fields or few main property of a contact',
|
||||
'icon' => 'fc-icon-wp_user_meta',
|
||||
],
|
||||
'add_contact_activity' => [
|
||||
'is_pro' => true,
|
||||
'type' => 'action',
|
||||
'category' => 'CRM',
|
||||
'title' => 'Add Notes & Activity',
|
||||
'description' => 'Add Notes or Activity to the Contact Profile',
|
||||
'icon' => 'fc-icon-writing'
|
||||
],
|
||||
'outgoing_webhook' => [
|
||||
'is_pro' => true,
|
||||
'type' => 'action',
|
||||
'category' => 'CRM',
|
||||
'title' => 'Outgoing Webhook',
|
||||
'description' => 'Send Data to external server via GET or POST Method',
|
||||
'icon' => 'fc-icon-webhooks',
|
||||
],
|
||||
'end_this_funnel' => [
|
||||
'is_pro' => true,
|
||||
'type' => 'action',
|
||||
'category' => 'CRM',
|
||||
'title' => 'End This Funnel Here',
|
||||
'description' => 'No further action will run once a contact hit this point',
|
||||
'icon' => 'fc-icon-end_funnel',
|
||||
],
|
||||
'fcrm_check_user_prop' => [
|
||||
'is_pro' => true,
|
||||
'type' => 'conditional',
|
||||
'title' => 'Check Contact\'s Properties',
|
||||
'description' => 'Check If the contact match specific data properties',
|
||||
'icon' => 'fc-icon-check_contact_property_conditional',
|
||||
],
|
||||
'user_registration_action' => [
|
||||
'is_pro' => true,
|
||||
'category' => 'WordPress',
|
||||
'type' => 'action',
|
||||
'title' => 'Create WordPress User',
|
||||
'description' => 'Create WP User with a role if user is not already registered with contact email',
|
||||
'icon' => 'fc-icon-create_wp_user',
|
||||
],
|
||||
'fcrm_update_user_meta' => [
|
||||
'is_pro' => true,
|
||||
'type' => 'action',
|
||||
'category' => 'WordPress',
|
||||
'title' => 'Update WP User Meta',
|
||||
'description' => 'Update WordPress User Meta Data',
|
||||
'icon' => 'fc-icon-wp_user_meta',
|
||||
],
|
||||
'fcrm_change_user_role' => [
|
||||
'is_pro' => true,
|
||||
'type' => 'action',
|
||||
'category' => 'WordPress',
|
||||
'title' => 'Change WP User Role',
|
||||
'description' => 'If user exist with the contact email then you can change user role',
|
||||
'icon' => 'fc-icon-wp_user_role',
|
||||
]
|
||||
];
|
||||
|
||||
if (Helper::isEdd3()) {
|
||||
$blocks['edd_update_payment_status_benchmark'] = [
|
||||
'is_pro' => true,
|
||||
'type' => 'conditional',
|
||||
'title' => 'New Order Success in EDD',
|
||||
'description' => 'This will run once new order will be placed as processing in EDD',
|
||||
'icon' => 'fc-icon-edd_new_order_success',
|
||||
];
|
||||
$blocks['fcrm_edd_is_purchased'] = [
|
||||
'is_pro' => true,
|
||||
'type' => 'conditional',
|
||||
'title' => 'Check if the contact purchased a specific product',
|
||||
'description' => 'Check If user purchased selected products and run sequences conditionally',
|
||||
'icon' => 'fc-icon-edd_new_order_success',
|
||||
];
|
||||
}
|
||||
|
||||
if (defined('WC_PLUGIN_FILE')) {
|
||||
$blocks['fcrm_woo_is_purchased'] = [
|
||||
'is_pro' => true,
|
||||
'type' => 'conditional',
|
||||
'title' => 'Check if the contact purchased a specific product',
|
||||
'description' => 'Check If user purchased selected products and run sequences conditionally',
|
||||
'icon' => 'fc-icon-woo_purchased',
|
||||
];
|
||||
|
||||
$blocks['woocommerce_order_status_processing_benchmark'] = [
|
||||
'is_pro' => true,
|
||||
'type' => 'benchmark',
|
||||
'title' => 'Order Received in WooCommerce',
|
||||
'description' => 'This will run once new order has been placed as processing',
|
||||
'icon' => 'fc-icon-new_order_woo',
|
||||
];
|
||||
}
|
||||
|
||||
if (defined('LEARNDASH_VERSION')) {
|
||||
$blocks['fcrm_learndhash_is_in_course'] = [
|
||||
'is_pro' => true,
|
||||
'type' => 'conditional',
|
||||
'title' => '[LearnDash] Check if the contact enroll a course',
|
||||
'description' => 'Conditionally check if contact enrolled or completed a course',
|
||||
'icon' => 'fc-icon-ld_in_course',
|
||||
];
|
||||
$blocks['fcrm_learndhash_is_in_group'] = [
|
||||
'is_pro' => true,
|
||||
'type' => 'conditional',
|
||||
'title' => '[LearnDash] Check if the contact is in a group',
|
||||
'description' => 'Conditionally check if contact is in a group',
|
||||
'icon' => 'fc-icon-ld_in_group',
|
||||
];
|
||||
$blocks['fcrm_learndhash_add_to_course'] = [
|
||||
'is_pro' => true,
|
||||
'type' => 'action',
|
||||
'category' => 'LearnDash',
|
||||
'title' => 'Enroll To Course',
|
||||
'description' => 'Enroll the contact to a specific LMS Course',
|
||||
'icon' => 'fc-icon-learndash_enroll_course',
|
||||
];
|
||||
$blocks['fcrm_learndhash_add_to_group'] = [
|
||||
'is_pro' => true,
|
||||
'type' => 'action',
|
||||
'category' => 'LearnDash',
|
||||
'title' => 'Enroll To Group',
|
||||
'description' => 'Enroll the contact to a specific LMS Group',
|
||||
'icon' => '',
|
||||
];
|
||||
}
|
||||
|
||||
if (defined('LLMS_PLUGIN_FILE')) {
|
||||
$blocks['fcrm_lifter_is_in_course'] = [
|
||||
'is_pro' => true,
|
||||
'type' => 'conditional',
|
||||
'title' => '[LifterLMS] Check if the contact enroll a course',
|
||||
'description' => 'Conditionally check if contact enrolled or completed a course',
|
||||
'icon' => 'fc-icon-lifter_lms_course_enrollment',
|
||||
];
|
||||
$blocks['fcrm_lifter_is_in_membership'] = [
|
||||
'is_pro' => true,
|
||||
'type' => 'conditional',
|
||||
'title' => '[LifterLMS] Check if the contact has a Membership',
|
||||
'description' => 'Conditionally check if contact has an active membership level',
|
||||
'icon' => 'fc-icon-lifter_lms_membership',
|
||||
];
|
||||
$blocks['fcrm_lifter_add_to_course'] = [
|
||||
'is_pro' => true,
|
||||
'type' => 'action',
|
||||
'category' => 'LifterLMS',
|
||||
'title' => 'Enroll To Course',
|
||||
'description' => 'Enroll the contact to a specific LMS Course',
|
||||
'icon' => '',
|
||||
];
|
||||
$blocks['fcrm_lifterlms_add_to_group'] = [
|
||||
'is_pro' => true,
|
||||
'type' => 'action',
|
||||
'category' => 'LifterLMS',
|
||||
'title' => 'Enroll To Membership',
|
||||
'description' => 'Enroll the contact to a specific LMS Membership',
|
||||
'icon' => '',
|
||||
];
|
||||
}
|
||||
|
||||
if (defined('TUTOR_VERSION')) {
|
||||
$blocks['fcrm_tutor_is_in_membership'] = [
|
||||
'is_pro' => true,
|
||||
'type' => 'conditional',
|
||||
'title' => '[TutorLMS] Check if the contact enroll a course',
|
||||
'description' => 'Conditionally check if contact enrolled or completed a course',
|
||||
'icon' => 'fc-icon-tutor_in_course',
|
||||
];
|
||||
}
|
||||
|
||||
if (defined('SURECART_PLUGIN_FILE')) {
|
||||
$blocks['fluent_surecart_purchase_created_wrap'] = [
|
||||
'is_pro' => true,
|
||||
'type' => 'benchmark',
|
||||
'title' => 'Order Received in SureCart',
|
||||
'description' => 'This will run once new order has been placed as processing',
|
||||
'icon' => 'el-icon-shopping-cart-full',
|
||||
];
|
||||
}
|
||||
|
||||
return $blocks;
|
||||
|
||||
}
|
||||
|
||||
private function getCrmTriggers()
|
||||
{
|
||||
return [
|
||||
'fluentcrm_contact_added_to_lists' => [
|
||||
'category' => 'CRM',
|
||||
'label' => 'List Applied',
|
||||
'icon' => 'fc-icon-list_applied_2',
|
||||
'description' => 'This will run when selected lists have been applied to a contact',
|
||||
'disabled' => true
|
||||
],
|
||||
|
||||
'fluentcrm_contact_removed_from_lists' => [
|
||||
'category' => 'CRM',
|
||||
'label' => 'List Removed',
|
||||
'icon' => 'fc-icon-list_removed',
|
||||
'description' => 'This will run when selected lists have been removed from a contact',
|
||||
'disabled' => true
|
||||
],
|
||||
|
||||
'fluentcrm_contact_added_to_tags' => [
|
||||
'category' => 'CRM',
|
||||
'label' => 'Tag Applied',
|
||||
'icon' => 'fc-icon-tag_applied',
|
||||
'description' => 'This will run when selected tags have been applied to a contact',
|
||||
'disabled' => true
|
||||
],
|
||||
|
||||
'fluentcrm_contact_removed_from_tags' => [
|
||||
'category' => 'CRM',
|
||||
'label' => 'Tag Removed',
|
||||
'icon' => 'fc-icon-tag_removed',
|
||||
'description' => 'This will run when selected Tags have been removed from a contact',
|
||||
'disabled' => true
|
||||
],
|
||||
|
||||
'fluentcrm_contact_birthday' => [
|
||||
'category' => 'CRM',
|
||||
'label' => 'Contact\'s Birthday',
|
||||
'icon' => '', // 'fc-icon-tag_applied',
|
||||
'description' => 'Funnel will be initiated on the day of contact\'s birthday',
|
||||
'disabled' => true
|
||||
],
|
||||
|
||||
'fluent_crm/contact_created' => [
|
||||
'category' => 'CRM',
|
||||
'label' => 'Contact Created',
|
||||
'icon' => '', // 'fc-icon-tag_applied',
|
||||
'description' => 'This will run when a new contact will be added',
|
||||
'disabled' => true
|
||||
],
|
||||
|
||||
'fluentcrm_contact_added_to_companies' => [
|
||||
'category' => 'CRM',
|
||||
'label' => 'Company Applied',
|
||||
'icon' => '', // 'fc-icon-tag_applied',
|
||||
'description' => 'This will run when selected companies have been applied to a contact',
|
||||
'disabled' => true
|
||||
],
|
||||
|
||||
'fluentcrm_contact_removed_from_companies' => [
|
||||
'category' => 'CRM',
|
||||
'label' => 'Company Removed',
|
||||
'icon' => '', // 'fc-icon-tag_applied',
|
||||
'description' => 'This will run when selected companies have been removed from a contact',
|
||||
'disabled' => true
|
||||
],
|
||||
|
||||
'wp_login' => [
|
||||
'category' => 'WordPress Triggers',
|
||||
'label' => 'User Login',
|
||||
'icon' => 'fc-icon-wp_new_user_signup',
|
||||
'description' => 'This Funnel will be initiated when a user login to your site',
|
||||
'disabled' => true
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel;
|
||||
|
||||
use FluentCrm\App\Models\FunnelSequence;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
class SequencePoints
|
||||
{
|
||||
private $nextSequence = null;
|
||||
|
||||
private $immediateSequences = [];
|
||||
|
||||
private $lastSequence = null;
|
||||
|
||||
private $requiredBenchMark = null;
|
||||
|
||||
private $funnel;
|
||||
|
||||
private $hasNext = null;
|
||||
|
||||
private $funnelSubscriber;
|
||||
|
||||
private $nextSequenceExecutionTime = false;
|
||||
|
||||
public $hasEndSequence = false;
|
||||
|
||||
public function __construct($funnel, $funnelSubscriber = false)
|
||||
{
|
||||
$this->funnel = $funnel;
|
||||
$this->funnelSubscriber = $funnelSubscriber;
|
||||
$this->setupData();
|
||||
}
|
||||
|
||||
private function setupData()
|
||||
{
|
||||
$this->resolveLastSequence();
|
||||
|
||||
$isInChild = false;
|
||||
$sequences = $this->queryRemainingSequences($isInChild);
|
||||
|
||||
if (!$sequences || $sequences->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$inWaitTimes = false;
|
||||
$this->classifySequences($sequences, $isInChild, $inWaitTimes);
|
||||
|
||||
// If we're inside a child block and exhausted all child sequences,
|
||||
// transition back to the parent-level sequence flow
|
||||
if ($isInChild && !$this->nextSequence && !$this->hasEndSequence) {
|
||||
$this->handleChildToParentTransition($inWaitTimes);
|
||||
}
|
||||
|
||||
if ($this->nextSequence && $this->nextSequenceExecutionTime) {
|
||||
$this->nextSequence->execution_date_time = $this->nextSequenceExecutionTime;
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveLastSequence()
|
||||
{
|
||||
if ($this->funnelSubscriber && $this->funnelSubscriber->last_sequence_id) {
|
||||
$this->lastSequence = FunnelSequence::where('id', $this->funnelSubscriber->last_sequence_id)->first();
|
||||
}
|
||||
}
|
||||
|
||||
private function queryRemainingSequences(&$isInChild)
|
||||
{
|
||||
if ($this->lastSequence) {
|
||||
return $this->queryFromLastSequence($isInChild);
|
||||
}
|
||||
|
||||
return $this->queryFromStart();
|
||||
}
|
||||
|
||||
private function queryFromLastSequence(&$isInChild)
|
||||
{
|
||||
$query = FunnelSequence::orderBy('sequence', 'ASC')
|
||||
->where('funnel_id', $this->funnel->id)
|
||||
->where('sequence', '>', $this->lastSequence->sequence);
|
||||
|
||||
if ($this->lastSequence->parent_id) {
|
||||
$isInChild = true;
|
||||
$query->where('parent_id', $this->lastSequence->parent_id)
|
||||
->where('condition_type', $this->lastSequence->condition_type);
|
||||
}
|
||||
|
||||
$sequences = $query->get();
|
||||
|
||||
// If inside a child block but no more child sequences, escape to parent level
|
||||
if ($isInChild && $sequences->isEmpty()) {
|
||||
$isInChild = false;
|
||||
$sequences = $this->queryParentEscapeSequences();
|
||||
}
|
||||
|
||||
return $sequences;
|
||||
}
|
||||
|
||||
private function queryParentEscapeSequences()
|
||||
{
|
||||
$nextSequenceNumber = $this->funnelSubscriber->next_sequence;
|
||||
|
||||
if ($this->funnelSubscriber->next_sequence_item) {
|
||||
$nextSequenceNumber = $this->funnelSubscriber->next_sequence_item->sequence;
|
||||
}
|
||||
|
||||
if (!$nextSequenceNumber) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return FunnelSequence::orderBy('sequence', 'ASC')
|
||||
->where('funnel_id', $this->funnel->id)
|
||||
->where('sequence', '>=', $nextSequenceNumber)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('parent_id')
|
||||
->orWhere('parent_id', '0');
|
||||
})
|
||||
->get();
|
||||
}
|
||||
|
||||
private function queryFromStart()
|
||||
{
|
||||
$nextSequenceNumber = $this->funnelSubscriber ? $this->funnelSubscriber->next_sequence : null;
|
||||
|
||||
return FunnelSequence::orderBy('sequence', 'ASC')
|
||||
->where('funnel_id', $this->funnel->id)
|
||||
->when($nextSequenceNumber, function ($q) use ($nextSequenceNumber) {
|
||||
$q->where('sequence', '>=', $nextSequenceNumber);
|
||||
})
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Single-pass classification of sequences into immediate, next, benchmark, and end categories.
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $sequences
|
||||
* @param bool $isInChild Whether we're inside a conditional child block
|
||||
* @param bool &$inWaitTimes Tracks whether a wait-time action has been encountered
|
||||
*/
|
||||
private function classifySequences($sequences, $isInChild, &$inWaitTimes)
|
||||
{
|
||||
$firstSequence = $sequences[0];
|
||||
$conditionalBlock = false;
|
||||
$hasEndSequence = false;
|
||||
$immediateSequences = [];
|
||||
|
||||
foreach ($sequences as $sequence) {
|
||||
if ($this->requiredBenchMark || $conditionalBlock || $hasEndSequence) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip orphaned child sequences when processing at parent level
|
||||
if (!$isInChild && $sequence->parent_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($sequence->action_name == 'fluentcrm_wait_times' && !$inWaitTimes) {
|
||||
$inWaitTimes = true;
|
||||
$funnelSubId = $this->funnelSubscriber ? $this->funnelSubscriber->id : null;
|
||||
$seconds = FunnelHelper::getCurrentDelayInSeconds($sequence->settings, $sequence, $funnelSubId);
|
||||
$this->nextSequenceExecutionTime = gmdate('Y-m-d H:i:s', current_time('timestamp') + $seconds);
|
||||
}
|
||||
|
||||
if ($sequence->type == 'benchmark') {
|
||||
if (Arr::get($sequence->settings, 'type') == 'required') {
|
||||
if (!$this->funnelSubscriber || !apply_filters('fluent_crm/benchmark_already_asserted_' . $sequence->action_name, false, $sequence, $this->funnelSubscriber)) {
|
||||
$this->requiredBenchMark = $sequence;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($sequence->type == 'conditional') {
|
||||
$conditionalBlock = $sequence;
|
||||
}
|
||||
|
||||
if ($sequence->c_delay == $firstSequence->c_delay) {
|
||||
$immediateSequences[] = $sequence;
|
||||
if ($sequence->action_name == 'end_this_funnel') {
|
||||
$hasEndSequence = true;
|
||||
}
|
||||
} else {
|
||||
if (!$this->nextSequence || $sequence->c_delay < $this->nextSequence->c_delay) {
|
||||
$this->hasNext = true;
|
||||
$this->nextSequence = $sequence;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($conditionalBlock) {
|
||||
$this->hasNext = true;
|
||||
}
|
||||
|
||||
$this->immediateSequences = array_merge($this->immediateSequences, $immediateSequences);
|
||||
$this->hasEndSequence = $hasEndSequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a child block is exhausted, find the next top-level sequences after the parent conditional.
|
||||
*/
|
||||
private function handleChildToParentTransition($inWaitTimes)
|
||||
{
|
||||
if (!$this->lastSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parentSequence = FunnelSequence::where('id', $this->lastSequence->parent_id)->first();
|
||||
|
||||
if (!$parentSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sequences = FunnelSequence::where('funnel_id', $this->funnel->id)
|
||||
->where('sequence', '>', $parentSequence->sequence)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('parent_id')
|
||||
->orWhere('parent_id', '0');
|
||||
})
|
||||
->orderBy('sequence', 'ASC')
|
||||
->get();
|
||||
|
||||
if ($sequences->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If a wait time was already applied inside the child block,
|
||||
// schedule the first parent sequence with that delay
|
||||
if ($inWaitTimes) {
|
||||
$this->hasNext = true;
|
||||
$this->nextSequence = $sequences[0];
|
||||
if ($this->nextSequenceExecutionTime) {
|
||||
$this->nextSequence->execution_date_time = $this->nextSequenceExecutionTime;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Classify the parent-level sequences using the same logic
|
||||
$this->classifySequences($sequences, false, $inWaitTimes);
|
||||
}
|
||||
|
||||
public function getCurrentSequences()
|
||||
{
|
||||
return $this->immediateSequences;
|
||||
}
|
||||
|
||||
public function getNextSequence()
|
||||
{
|
||||
return $this->nextSequence;
|
||||
}
|
||||
|
||||
public function hasNext()
|
||||
{
|
||||
return $this->hasNext || !!$this->nextSequence;
|
||||
}
|
||||
|
||||
public function getRequiredBenchmark()
|
||||
{
|
||||
return $this->requiredBenchMark;
|
||||
}
|
||||
|
||||
public function hasSequences()
|
||||
{
|
||||
return !!$this->requiredBenchMark || !!$this->immediateSequences;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel;
|
||||
|
||||
class StaticTemplates
|
||||
{
|
||||
public static function get()
|
||||
{
|
||||
return [
|
||||
[
|
||||
'id' => 1,
|
||||
'label' => __('Welcome Email', 'fluent-crm'),
|
||||
'description' => __('Send a welcome email to new subscribers', 'fluent-crm'),
|
||||
'category' => 'email',
|
||||
'icon' => 'el-icon-message',
|
||||
'disabled' => false,
|
||||
'depends_on' => ['crm', 'email'],
|
||||
'ribbon' => __('Free', 'fluent-crm'),
|
||||
'funnel_data' => [
|
||||
"id" => 20,
|
||||
"type" => "funnels",
|
||||
"title" => "List Applied (Created at 2024-07-03)",
|
||||
"trigger_name" => "fluentcrm_contact_added_to_lists",
|
||||
"status" => "published",
|
||||
"conditions" => [
|
||||
"run_multiple" => "no",
|
||||
],
|
||||
"settings" => [
|
||||
"lists" => [
|
||||
],
|
||||
"select_type" => "any",
|
||||
],
|
||||
"created_by" => "1",
|
||||
"created_at" => "2024-07-03 11:10:24",
|
||||
"updated_at" => "2024-07-03 17:56:07",
|
||||
"settingsFields" => [
|
||||
"title" => __("List Applied", 'fluent-crm'),
|
||||
"sub_title" => __("This will run when selected lists have been applied to a contact", 'fluent-crm'),
|
||||
"fields" => [
|
||||
"lists" => [
|
||||
"type" => "option_selectors",
|
||||
"option_key" => "lists",
|
||||
"is_multiple" => true,
|
||||
"label" => __("Select Lists", 'fluent-crm'),
|
||||
"placeholder" => __("Select List", 'fluent-crm'),
|
||||
"creatable" => true,
|
||||
],
|
||||
"select_type" => [
|
||||
"label" => __("Run When", 'fluent-crm'),
|
||||
"type" => "radio",
|
||||
"options" => [
|
||||
[
|
||||
"id" => "any",
|
||||
"title" => __("contact added in any of the selected lists", 'fluent-crm'),
|
||||
],
|
||||
[
|
||||
"id" => "all",
|
||||
"title" => __("contact added in all of the selected lists", 'fluent-crm'),
|
||||
],
|
||||
],
|
||||
"dependency" => [
|
||||
"depends_on" => "lists",
|
||||
"operator" => "!=",
|
||||
"value" => [
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
"conditionFields" => [
|
||||
"run_multiple" => [
|
||||
"type" => "yes_no_check",
|
||||
"label" => "",
|
||||
"check_label" => __("Restart the Automation Multiple times for a contact for this event. (Only enable if you want to restart automation for the same contact)", 'fluent-crm'),
|
||||
"inline_help" => __("If you enable, then it will restart the automation for a contact if the contact already in the automation. Otherwise, It will just skip if already exist", 'fluent-crm'),
|
||||
],
|
||||
],
|
||||
"sequences" => [
|
||||
[
|
||||
"id" => 21,
|
||||
"funnel_id" => "20",
|
||||
"parent_id" => "0",
|
||||
"action_name" => "add_contact_to_company",
|
||||
"condition_type" => null,
|
||||
"type" => "action",
|
||||
"title" => __("Apply Company", 'fluent-crm'),
|
||||
"description" => __("Add contact to the selected company", 'fluent-crm'),
|
||||
"status" => "published",
|
||||
"conditions" => [
|
||||
],
|
||||
"settings" => [
|
||||
"company" => null,
|
||||
],
|
||||
"note" => null,
|
||||
"delay" => "0",
|
||||
"c_delay" => "0",
|
||||
"sequence" => "1",
|
||||
"created_by" => "1",
|
||||
"created_at" => "2024-07-03 17:55:55",
|
||||
"updated_at" => "2024-07-03 17:56:07",
|
||||
],
|
||||
[
|
||||
"id" => 22,
|
||||
"funnel_id" => "20",
|
||||
"parent_id" => "0",
|
||||
"action_name" => "fluentcrm_wait_times",
|
||||
"condition_type" => null,
|
||||
"type" => "action",
|
||||
"title" => __("Wait X Days/Hours", 'fluent-crm'),
|
||||
"description" => __("Wait defined timespan before execute the next action", 'fluent-crm'),
|
||||
"status" => "published",
|
||||
"conditions" => [
|
||||
],
|
||||
"settings" => [
|
||||
"wait_type" => "unit_wait",
|
||||
"wait_time_amount" => 0,
|
||||
"wait_time_unit" => "days",
|
||||
"is_timestamp_wait" => "",
|
||||
"wait_date_time" => "",
|
||||
"to_day" => [
|
||||
],
|
||||
"to_day_time" => "",
|
||||
],
|
||||
"note" => null,
|
||||
"delay" => "0",
|
||||
"c_delay" => "0",
|
||||
"sequence" => "2",
|
||||
"created_by" => "1",
|
||||
"created_at" => "2024-07-03 17:56:01",
|
||||
"updated_at" => "2024-07-03 17:56:07",
|
||||
],
|
||||
],
|
||||
"site_hash" => "74401346bac500b8b0e449bdc75b2255",
|
||||
"export_date" => "2024-07-03 11:56:26",
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel\Triggers;
|
||||
|
||||
use FluentCrm\App\Services\Funnel\BaseTrigger;
|
||||
use FluentCrm\App\Services\Funnel\FunnelHelper;
|
||||
use FluentCrm\App\Services\Funnel\FunnelProcessor;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
use FluentForm\App\Modules\Form\FormFieldsParser;
|
||||
use FluentForm\App\Services\FormBuilder\ShortCodeParser;
|
||||
|
||||
class FluentFormSubmissionTrigger extends BaseTrigger
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
if (!defined('FLUENTFORM')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$this->actionArgNum = 3;
|
||||
$this->triggerName = 'fluentform_submission_inserted';
|
||||
$this->priority = 25;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getTrigger()
|
||||
{
|
||||
return [
|
||||
'category' => __('Fluent Forms', 'fluent-crm'),
|
||||
'label' => __('New Form Submission (Fluent Forms)', 'fluent-crm'),
|
||||
'description' => __('This Funnel will be initiated when a new form submission has been submitted', 'fluent-crm'),
|
||||
'icon' => 'fc-icon-fluentforms',
|
||||
];
|
||||
}
|
||||
|
||||
public function getFunnelSettingsDefaults()
|
||||
{
|
||||
return [
|
||||
'form_id' => '',
|
||||
'primary_fields' => [
|
||||
'first_name' => '',
|
||||
'last_name' => '',
|
||||
'email' => ''
|
||||
],
|
||||
'other_fields' => [
|
||||
[
|
||||
'field_key' => '',
|
||||
'field_value' => ''
|
||||
]
|
||||
],
|
||||
'subscription_status' => 'subscribed'
|
||||
];
|
||||
}
|
||||
|
||||
public function getSettingsFields($funnel)
|
||||
{
|
||||
$valueOptions = $this->getValueOptions($funnel);
|
||||
|
||||
$formId = Arr::get($funnel->settings, 'form_id');
|
||||
|
||||
$subtitle = __('This Funnel will be initiated when a new form submission has been submitted.', 'fluent-crm');
|
||||
|
||||
if ($formId) {
|
||||
$subtitle .= ' Use shortcode <b> [fluentform id="' . $formId . '"] </b> to show the form in your WordPress page/posts. <a target="_blank" href="' . admin_url('admin.php?page=fluent_forms&route=editor&form_id=' . $formId) . '">Edit The Form</a>';
|
||||
}
|
||||
|
||||
$secondaryFields = apply_filters('fluent_crm/fluentform_other_map_fields',
|
||||
FunnelHelper::getSecondaryContactFieldMaps()
|
||||
);
|
||||
|
||||
return [
|
||||
'title' => __('New Fluent Forms Submission Funnel', 'fluent-crm'),
|
||||
'sub_title' => $subtitle,
|
||||
'fields' => [
|
||||
'form_id' => [
|
||||
'type' => 'reload_field_selection',
|
||||
'label' => __('Select your form', 'fluent-crm'),
|
||||
'options' => $this->getForms($funnel)
|
||||
],
|
||||
'primary_fields' => [
|
||||
'label' => __('Map Primary Data', 'fluent-crm'),
|
||||
'type' => 'form-group-mapper',
|
||||
'value_options' => $valueOptions,
|
||||
'local_label' => __('Contact Field (CRM)', 'fluent-crm'),
|
||||
'remote_label' => __('Form Field', 'fluent-crm'),
|
||||
'fields' => FunnelHelper::getPrimaryContactFieldMaps()
|
||||
],
|
||||
'other_fields' => [
|
||||
'label' => __('Map Other Data', 'fluent-crm'),
|
||||
'type' => 'form-many-drop-down-mapper',
|
||||
'value_options' => $valueOptions,
|
||||
'local_label' => __('Select Contact Property', 'fluent-crm'),
|
||||
'remote_label' => __('Select Form Field', 'fluent-crm'),
|
||||
'local_placeholder' => __('Select Contact Property', 'fluent-crm'),
|
||||
'remote_placeholder' => __('Select Form Property', 'fluent-crm'),
|
||||
'fields' => $secondaryFields
|
||||
],
|
||||
'subscription_status' => [
|
||||
'type' => 'select',
|
||||
'is_multiple' => false,
|
||||
'options' => [
|
||||
[
|
||||
'id' => 'subscribed',
|
||||
'title' => 'Subscribed'
|
||||
],
|
||||
[
|
||||
'id' => 'pending',
|
||||
'title' => 'Pending'
|
||||
]
|
||||
],
|
||||
'label' => __('Subscription Status', 'fluent-crm'),
|
||||
'placeholder' => __('Select Status', 'fluent-crm')
|
||||
],
|
||||
'subscription_status_info' => [
|
||||
'type' => 'html',
|
||||
'info' => '<b>An Automated double-optin email will be sent if the contact is new or not subscribed already</b>',
|
||||
'dependency' => [
|
||||
'depends_on' => 'subscription_status',
|
||||
'operator' => '=',
|
||||
'value' => 'pending'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
protected function getForms($funnel)
|
||||
{
|
||||
return fluentCrmDb()->table('fluentform_forms')
|
||||
->select('id', 'title')
|
||||
->orderBy('id', 'DESC')
|
||||
->get();
|
||||
}
|
||||
|
||||
protected function getValueOptions($funnel)
|
||||
{
|
||||
$formId = Arr::get($funnel->settings, 'form_id');
|
||||
if (!$formId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$form = fluentCrmDb()->table('fluentform_forms')
|
||||
->find($formId);
|
||||
|
||||
if(!$form) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$formFields = FormFieldsParser::getShortCodeInputs(
|
||||
$form, [
|
||||
'admin_label'
|
||||
]);
|
||||
|
||||
$formattedInputs = [];
|
||||
|
||||
foreach ($formFields as $inputKey => $input) {
|
||||
$formattedInputs[] = [
|
||||
'id' => '{inputs.' . $inputKey . '}',
|
||||
'title' => $input['admin_label']
|
||||
];
|
||||
}
|
||||
|
||||
return $formattedInputs;
|
||||
}
|
||||
|
||||
public function getFunnelConditionDefaults($funnel)
|
||||
{
|
||||
return [
|
||||
'run_only_one' => 'yes'
|
||||
];
|
||||
}
|
||||
|
||||
public function getConditionFields($funnel)
|
||||
{
|
||||
return [
|
||||
'run_only_one' => [
|
||||
'type' => 'yes_no_check',
|
||||
'label' => '',
|
||||
'check_label' => __('Run this automation only once per contact. If unchecked then it will over-write existing flow', 'fluent-crm'),
|
||||
'help' => __('If you enable this then this will run only once per customer otherwise, It will delete the existing automation flow and start new', 'fluent-crm'),
|
||||
'options' => FunnelHelper::getUpdateOptions()
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function handle($funnel, $originalArgs)
|
||||
{
|
||||
$insertId = $originalArgs[0];
|
||||
$formData = $originalArgs[1];
|
||||
$form = $originalArgs[2];
|
||||
|
||||
$processedValues = $funnel->settings;
|
||||
|
||||
if (Arr::get($processedValues, 'form_id') != $form->id) {
|
||||
return; // not our form
|
||||
}
|
||||
|
||||
$processedValues['primary_fields']['ip'] = '{submission.ip}';
|
||||
|
||||
$processedValues = ShortCodeParser::parse($processedValues, $insertId, $formData);
|
||||
|
||||
if (!is_email(Arr::get($processedValues, 'primary_fields.email'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subscriberData = Arr::get($processedValues, 'primary_fields', []);
|
||||
|
||||
$subscriberData['custom_values'] = [];
|
||||
|
||||
foreach (Arr::get($processedValues, 'other_fields', []) as $otherField) {
|
||||
if (!empty($otherField['field_key']) && !empty($otherField['field_value'])) {
|
||||
$key = $otherField['field_key'];
|
||||
if (strpos($key, '.') !== false) {
|
||||
$subscriberData['custom_values'][str_replace('custom.', '', $key)] = $otherField['field_value'];
|
||||
} else {
|
||||
$subscriberData[$key] = $otherField['field_value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$willProcess = $this->isProcessable($funnel, $subscriberData);
|
||||
|
||||
$willProcess = apply_filters('fluentcrm_funnel_will_process_' . $this->triggerName, $willProcess, $funnel, $subscriberData, $originalArgs);
|
||||
if (!$willProcess) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subscriberData['status'] = $processedValues['subscription_status'];
|
||||
|
||||
$entry = fluentFormApi('submissions')->find($insertId);
|
||||
|
||||
if ($entry && $entry->status == 'confirmed') {
|
||||
$subscriberData['status'] = 'subscribed';
|
||||
}
|
||||
|
||||
if (!empty($subscriberData['country'])) {
|
||||
$country = FunnelHelper::getCountryShortName($subscriberData['country']);
|
||||
if ($country) {
|
||||
$subscriberData['country'] = $country;
|
||||
} else {
|
||||
unset($subscriberData['country']);
|
||||
}
|
||||
}
|
||||
|
||||
(new FunnelProcessor())->startFunnelSequence($funnel, $subscriberData, [
|
||||
'source_trigger_name' => $this->triggerName,
|
||||
'source_ref_id' => $insertId,
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
private function isProcessable($funnel, $subscriberData)
|
||||
{
|
||||
$subscriber = FunnelHelper::getSubscriber($subscriberData['email']);
|
||||
if ($subscriber && FunnelHelper::ifAlreadyInFunnel($funnel->id, $subscriber->id)) {
|
||||
$conditions = $funnel->conditions;
|
||||
// check update_type
|
||||
$runMultiple = Arr::get($conditions, 'run_only_one') == 'no';
|
||||
|
||||
// if run multiple then delete
|
||||
if ($runMultiple) {
|
||||
FunnelHelper::removeSubscribersFromFunnel($funnel->id, [$subscriber->id]);
|
||||
}
|
||||
|
||||
return $runMultiple;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel\Triggers;
|
||||
|
||||
use FluentCrm\App\Services\Funnel\BaseTrigger;
|
||||
use FluentCrm\App\Services\Funnel\FunnelHelper;
|
||||
use FluentCrm\App\Services\Funnel\FunnelProcessor;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
class FluentFormSubscriptionCancelledTrigger extends BaseTrigger
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
if (!defined('FLUENTFORM')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$this->actionArgNum = 3;
|
||||
$this->triggerName = 'fluentform/subscription_payment_canceled';
|
||||
$this->priority = 25;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getTrigger()
|
||||
{
|
||||
return [
|
||||
'category' => __('Fluent Forms', 'fluent-crm'),
|
||||
'label' => __('Subscription Cancelled (Fluent Forms)', 'fluent-crm'),
|
||||
'description' => __('This Funnel will be initiated when a subscription is cancelled via Fluent Forms.', 'fluent-crm'),
|
||||
'icon' => 'fc-icon-fluentforms',
|
||||
];
|
||||
}
|
||||
|
||||
public function getFunnelSettingsDefaults()
|
||||
{
|
||||
return [
|
||||
'subscription_status' => 'subscribed'
|
||||
];
|
||||
}
|
||||
|
||||
public function getSettingsFields($funnel)
|
||||
{
|
||||
return [
|
||||
'title' => __('Subscription Cancelled Funnel', 'fluent-crm'),
|
||||
'sub_title' => __('This Funnel will be initiated when a subscription is cancelled via Fluent Forms.', 'fluent-crm'),
|
||||
'fields' => [
|
||||
'subscription_status' => [
|
||||
'type' => 'select',
|
||||
'is_multiple' => false,
|
||||
'options' => [
|
||||
[
|
||||
'id' => 'subscribed',
|
||||
'title' => __('Subscribed', 'fluent-crm')
|
||||
],
|
||||
[
|
||||
'id' => 'pending',
|
||||
'title' => __('Pending', 'fluent-crm')
|
||||
]
|
||||
],
|
||||
'label' => __('Subscription Status', 'fluent-crm'),
|
||||
'placeholder' => __('Select Status', 'fluent-crm')
|
||||
],
|
||||
'subscription_status_info' => [
|
||||
'type' => 'html',
|
||||
'info' => sprintf('<b>%s</b>', __('An automated double-opt-in email will be sent if the contact is new or not already subscribed.', 'fluent-crm')),
|
||||
'dependency' => [
|
||||
'depends_on' => 'subscription_status',
|
||||
'operator' => '=',
|
||||
'value' => 'pending'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
protected function getForms($funnel)
|
||||
{
|
||||
return fluentCrmDb()->table('fluentform_forms')
|
||||
->select('id', 'title')
|
||||
->orderBy('id', 'DESC')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getFunnelConditionDefaults($funnel)
|
||||
{
|
||||
return [
|
||||
'form_ids' => []
|
||||
];
|
||||
}
|
||||
|
||||
public function getConditionFields($funnel)
|
||||
{
|
||||
return [
|
||||
'form_ids' => [
|
||||
'type' => 'multi-select',
|
||||
'label' => __('Target Forms', 'fluent-crm'),
|
||||
'options' => $this->getForms($funnel),
|
||||
'inline_help' => __('Keep it blank to run for any form', 'fluent-crm')
|
||||
],
|
||||
'run_multiple' => [
|
||||
'type' => 'yes_no_check',
|
||||
'label' => '',
|
||||
'check_label' => __('Restart the Automation Multiple times for a contact for this event. (Only enable if you want to restart automation for the same contact)', 'fluent-crm'),
|
||||
'inline_help' => __('If you enable, then it will restart the automation for a contact if the contact already in the automation. Otherwise, It will just skip if already exist', 'fluent-crm')
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function handle($funnel, $originalArgs)
|
||||
{
|
||||
[$subscription, $submission, $vendorData] = $originalArgs;
|
||||
|
||||
$userId = $submission->user_id;
|
||||
|
||||
$subscriberData = FunnelHelper::prepareUserData($userId);
|
||||
|
||||
if (empty($subscriberData['email'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$willProcess = $this->isProcessable($funnel, $subscription, $subscriberData);
|
||||
|
||||
$willProcess = apply_filters('fluentcrm_funnel_will_process_' . $this->triggerName, $willProcess, $funnel, $subscriberData, $originalArgs);
|
||||
if (!$willProcess) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subscriberData = wp_parse_args($subscriberData, $funnel->settings);
|
||||
|
||||
$subscriberData['status'] = $subscriberData['subscription_status'];
|
||||
unset($subscriberData['subscription_status']);
|
||||
|
||||
(new FunnelProcessor())->startFunnelSequence($funnel, $subscriberData, [
|
||||
'source_trigger_name' => $this->triggerName,
|
||||
'source_ref_id' => $subscription->id
|
||||
]);
|
||||
}
|
||||
|
||||
private function isProcessable($funnel, $subscription, $subscriberData)
|
||||
{
|
||||
$conditions = $funnel->conditions;
|
||||
// check the Form Ids
|
||||
if ($conditions['form_ids']) {
|
||||
if (!in_array($subscription->form_id, $conditions['form_ids'])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$subscriber = FunnelHelper::getSubscriber($subscriberData['email']);
|
||||
|
||||
// check run_only_one
|
||||
if ($subscriber && FunnelHelper::ifAlreadyInFunnel($funnel->id, $subscriber->id)) {
|
||||
$multipleRun = Arr::get($conditions, 'run_multiple') == 'yes';
|
||||
if ($multipleRun) {
|
||||
FunnelHelper::removeSubscribersFromFunnel($funnel->id, [$subscriber->id]);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
namespace FluentCrm\App\Services\Funnel\Triggers;
|
||||
|
||||
|
||||
use FluentCrm\App\Services\Funnel\BaseTrigger;
|
||||
use FluentCrm\App\Services\Funnel\FunnelHelper;
|
||||
use FluentCrm\App\Services\Funnel\FunnelProcessor;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
use FluentForm\App\Modules\Form\FormFieldsParser;
|
||||
use FluentForm\App\Services\FormBuilder\ShortCodeParser;
|
||||
|
||||
class FluentFormSubscriptionPaymentReceivedTrigger extends BaseTrigger
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
if (!defined('FLUENTFORM')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->actionArgNum = 2;
|
||||
$this->triggerName = 'fluentform/subscription_payment_active';
|
||||
$this->priority = 25;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getTrigger()
|
||||
{
|
||||
return [
|
||||
'category' => __('Fluent Forms', 'fluent-crm'),
|
||||
'label' => __('Subscription Payment Received (Fluent Forms)', 'fluent-crm'),
|
||||
'description' => __('This Funnel will be initiated when a subscription payment is successfully received through Fluent Forms.', 'fluent-crm'),
|
||||
'icon' => 'fc-icon-fluentforms',
|
||||
];
|
||||
}
|
||||
|
||||
public function getFunnelSettingsDefaults()
|
||||
{
|
||||
return [
|
||||
'form_id' => '',
|
||||
'primary_fields' => [
|
||||
'first_name' => '',
|
||||
'last_name' => '',
|
||||
'email' => ''
|
||||
],
|
||||
'other_fields' => [
|
||||
[
|
||||
'field_key' => '',
|
||||
'field_value' => ''
|
||||
]
|
||||
],
|
||||
'subscription_status' => 'subscribed'
|
||||
];
|
||||
}
|
||||
|
||||
public function getSettingsFields($funnel)
|
||||
{
|
||||
$valueOptions = $this->getValueOptions($funnel);
|
||||
$secondaryFields = apply_filters('fluent_crm/fluentform_other_map_fields',
|
||||
FunnelHelper::getSecondaryContactFieldMaps()
|
||||
);
|
||||
|
||||
return [
|
||||
'title' => __('Subscription Payment Received', 'fluent-crm'),
|
||||
'sub_title' => __('This Funnel will be initiated when a subscription payment is successfully received through Fluent Forms.', 'fluent-crm'),
|
||||
'fields' => [
|
||||
'form_id' => [
|
||||
'type' => 'reload_field_selection',
|
||||
'label' => __('Select your form', 'fluent-crm'),
|
||||
'options' => $this->getForms($funnel)
|
||||
],
|
||||
'primary_fields' => [
|
||||
'label' => __('Map Primary Data', 'fluent-crm'),
|
||||
'type' => 'form-group-mapper',
|
||||
'value_options' => $valueOptions,
|
||||
'local_label' => __('Contact Field (CRM)', 'fluent-crm'),
|
||||
'remote_label' => __('Form Field', 'fluent-crm'),
|
||||
'fields' => FunnelHelper::getPrimaryContactFieldMaps()
|
||||
],
|
||||
'other_fields' => [
|
||||
'label' => __('Map Other Data', 'fluent-crm'),
|
||||
'type' => 'form-many-drop-down-mapper',
|
||||
'value_options' => $valueOptions,
|
||||
'local_label' => __('Select Contact Property', 'fluent-crm'),
|
||||
'remote_label' => __('Select Form Field', 'fluent-crm'),
|
||||
'local_placeholder' => __('Select Contact Property', 'fluent-crm'),
|
||||
'remote_placeholder' => __('Select Form Property', 'fluent-crm'),
|
||||
'fields' => $secondaryFields
|
||||
],
|
||||
'subscription_status' => [
|
||||
'type' => 'select',
|
||||
'is_multiple' => false,
|
||||
'options' => [
|
||||
[
|
||||
'id' => 'subscribed',
|
||||
'title' => 'Subscribed'
|
||||
],
|
||||
[
|
||||
'id' => 'pending',
|
||||
'title' => 'Pending'
|
||||
]
|
||||
],
|
||||
'label' => __('Subscription Status', 'fluent-crm'),
|
||||
'placeholder' => __('Select Status', 'fluent-crm')
|
||||
],
|
||||
'subscription_status_info' => [
|
||||
'type' => 'html',
|
||||
'info' => '<b>An Automated double-optin email will be sent if the contact is new or not subscribed already</b>',
|
||||
'dependency' => [
|
||||
'depends_on' => 'subscription_status',
|
||||
'operator' => '=',
|
||||
'value' => 'pending'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
protected function getValueOptions($funnel)
|
||||
{
|
||||
$formId = Arr::get($funnel->settings, 'form_id');
|
||||
if (!$formId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$form = fluentCrmDb()->table('fluentform_forms')
|
||||
->find($formId);
|
||||
|
||||
if(!$form) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$formFields = FormFieldsParser::getShortCodeInputs(
|
||||
$form, [
|
||||
'admin_label'
|
||||
]);
|
||||
|
||||
$formattedInputs = [];
|
||||
|
||||
foreach ($formFields as $inputKey => $input) {
|
||||
$formattedInputs[] = [
|
||||
'id' => '{inputs.' . $inputKey . '}',
|
||||
'title' => $input['admin_label']
|
||||
];
|
||||
}
|
||||
|
||||
return $formattedInputs;
|
||||
}
|
||||
|
||||
protected function getForms($funnel)
|
||||
{
|
||||
return fluentCrmDb()->table('fluentform_forms')
|
||||
->select('id', 'title')
|
||||
->orderBy('id', 'DESC')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getFunnelConditionDefaults($funnel)
|
||||
{
|
||||
return [
|
||||
'run_only_one' => 'yes'
|
||||
];
|
||||
}
|
||||
|
||||
public function getConditionFields($funnel)
|
||||
{
|
||||
return [
|
||||
'run_only_one' => [
|
||||
'type' => 'yes_no_check',
|
||||
'label' => '',
|
||||
'check_label' => __('Run this automation only once per contact. If unchecked then it will over-write existing flow', 'fluent-crm'),
|
||||
'help' => __('If you enable this then this will run only once per customer otherwise, It will delete the existing automation flow and start new', 'fluent-crm'),
|
||||
'options' => FunnelHelper::getUpdateOptions()
|
||||
],
|
||||
];
|
||||
}
|
||||
public function handle($funnel, $originalArgs)
|
||||
{
|
||||
[$submission, $formResponse] = $originalArgs;
|
||||
$formData = $formResponse->response;
|
||||
|
||||
$processedValues = $funnel->settings;
|
||||
$insertId = $submission->submission_id;
|
||||
|
||||
if (Arr::get($processedValues, 'form_id') != $formResponse->form_id) {
|
||||
return; // not our form
|
||||
}
|
||||
|
||||
$processedValues = ShortCodeParser::parse($processedValues, $insertId, $formData);
|
||||
|
||||
$subscriberData = Arr::get($processedValues, 'primary_fields', []);
|
||||
$subscriberData['custom_values'] = $this->processOtherFields(Arr::get($processedValues, 'other_fields', []));
|
||||
|
||||
$willProcess = $this->isProcessable($funnel, $subscriberData);
|
||||
|
||||
$willProcess = apply_filters('fluentcrm_funnel_will_process_' . $this->triggerName, $willProcess, $funnel, $subscriberData, $originalArgs);
|
||||
if (!$willProcess) {
|
||||
return;
|
||||
}
|
||||
$subscriberData['status'] = $processedValues['subscription_status'];
|
||||
|
||||
(new FunnelProcessor())->startFunnelSequence($funnel, $subscriberData, [
|
||||
'source_trigger_name' => $this->triggerName,
|
||||
'source_ref_id' => $insertId,
|
||||
]);
|
||||
}
|
||||
|
||||
private function processOtherFields(array $otherFields)
|
||||
{
|
||||
$customValues = [];
|
||||
foreach ($otherFields as $otherField) {
|
||||
if (!empty($otherField['field_key']) && !empty($otherField['field_value'])) {
|
||||
$key = $otherField['field_key'];
|
||||
if (strpos($key, '.') !== false) {
|
||||
$customValues[str_replace('custom.', '', $key)] = $otherField['field_value'];
|
||||
} else {
|
||||
$customValues[$key] = $otherField['field_value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $customValues;
|
||||
}
|
||||
|
||||
private function isProcessable($funnel, $subscriberData)
|
||||
{
|
||||
$subscriber = FunnelHelper::getSubscriber($subscriberData['email']);
|
||||
if ($subscriber && FunnelHelper::ifAlreadyInFunnel($funnel->id, $subscriber->id)) {
|
||||
$conditions = $funnel->conditions;
|
||||
// check update_type
|
||||
$runMultiple = Arr::get($conditions, 'run_only_one') == 'no';
|
||||
|
||||
// if run multiple then delete
|
||||
if ($runMultiple) {
|
||||
FunnelHelper::removeSubscribersFromFunnel($funnel->id, [$subscriber->id]);
|
||||
}
|
||||
|
||||
return $runMultiple;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\Funnel\Triggers;
|
||||
|
||||
use FluentCrm\App\Services\Funnel\BaseTrigger;
|
||||
use FluentCrm\App\Services\Funnel\FunnelHelper;
|
||||
use FluentCrm\App\Services\Funnel\FunnelProcessor;
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
class UserRegistrationTrigger extends BaseTrigger
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->triggerName = 'user_register';
|
||||
$this->priority = 10;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getTrigger()
|
||||
{
|
||||
return [
|
||||
'category' => __('WordPress Triggers', 'fluent-crm'),
|
||||
'label' => __('New User Sign Up', 'fluent-crm'),
|
||||
'description' => __('This Funnel will be initiated when a new user has been registered in your site', 'fluent-crm'),
|
||||
'icon' => 'fc-icon-wp_new_user_signup',
|
||||
];
|
||||
}
|
||||
|
||||
public function getFunnelSettingsDefaults()
|
||||
{
|
||||
return [
|
||||
'subscription_status' => 'subscribed'
|
||||
];
|
||||
}
|
||||
|
||||
public function getSettingsFields($funnel)
|
||||
{
|
||||
return [
|
||||
'title' => __('New User Sign Up Funnel', 'fluent-crm'),
|
||||
'sub_title' => __('This Funnel will be initiated when a new user has been registered in your site', 'fluent-crm'),
|
||||
'fields' => [
|
||||
'subscription_status' => [
|
||||
'type' => 'option_selectors',
|
||||
'option_key' => 'editable_statuses',
|
||||
'is_multiple' => false,
|
||||
'label' => __('Subscription Status', 'fluent-crm'),
|
||||
'placeholder' => __('Select Status', 'fluent-crm')
|
||||
],
|
||||
'subscription_status_info' => [
|
||||
'type' => 'html',
|
||||
'info' => '<b>'.__('An Automated double-optin email will be sent for new subscribers', 'fluent-crm').'</b>',
|
||||
'dependency' => [
|
||||
'depends_on' => 'subscription_status',
|
||||
'operator' => '=',
|
||||
'value' => 'pending'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function getFunnelConditionDefaults($funnel)
|
||||
{
|
||||
return [
|
||||
'update_type' => 'update', // skip_all_actions, skip_update_if_exist
|
||||
'user_roles' => []
|
||||
];
|
||||
}
|
||||
|
||||
public function getConditionFields($funnel)
|
||||
{
|
||||
return [
|
||||
'update_type' => [
|
||||
'type' => 'radio',
|
||||
'label' => __('If Contact Already Exist?', 'fluent-crm'),
|
||||
'help' => __('Please specify what will happen if the subscriber already exist in the database', 'fluent-crm'),
|
||||
'options' => FunnelHelper::getUpdateOptions()
|
||||
],
|
||||
'user_roles' => [
|
||||
'type' => 'multi-select',
|
||||
'is_multiple' => true,
|
||||
'label' => __('Targeted User Roles', 'fluent-crm'),
|
||||
'help' => __('Select which roles registration will run this automation Funnel', 'fluent-crm'),
|
||||
'placeholder' => __('Select Roles', 'fluent-crm'),
|
||||
'options' => FunnelHelper::getUserRoles(),
|
||||
'inline_help' => __('Leave blank to run for all user roles', 'fluent-crm')
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function handle($funnel, $originalArgs)
|
||||
{
|
||||
$userId = $originalArgs[0];
|
||||
$subscriberData = FunnelHelper::prepareUserData($userId);
|
||||
if (empty($subscriberData['email'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$willProcess = $this->isProcessable($funnel, $subscriberData);
|
||||
$willProcess = apply_filters('fluentcrm_funnel_will_process_' . $this->triggerName, $willProcess, $funnel, $subscriberData, $originalArgs);
|
||||
if (!$willProcess) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subscriberData = wp_parse_args($subscriberData, $funnel->settings);
|
||||
$subscriberData['status'] = $subscriberData['subscription_status'];
|
||||
unset($subscriberData['subscription_status']);
|
||||
|
||||
(new FunnelProcessor())->startFunnelSequence($funnel, $subscriberData, [
|
||||
'source_trigger_name' => $this->triggerName,
|
||||
'source_ref_id' => $userId
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
private function isProcessable($funnel, $subscriberData)
|
||||
{
|
||||
$user = get_user_by('ID', $subscriberData['user_id']);
|
||||
|
||||
$conditions = $funnel->conditions;
|
||||
// check update_type
|
||||
$updateType = Arr::get($conditions, 'update_type');
|
||||
|
||||
$subscriber = FunnelHelper::getSubscriber($subscriberData['email']);
|
||||
if ($updateType == 'skip_all_if_exist' && $subscriber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check run_only_one
|
||||
if ($subscriber && FunnelHelper::ifAlreadyInFunnel($funnel->id, $subscriber->id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check user roles
|
||||
if ($roles = Arr::get($conditions, 'user_roles', [])) {
|
||||
$userRoles = array_values($user->roles);
|
||||
return !!array_intersect($userRoles, $roles);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user