Initial commit

This commit is contained in:
gustavooth
2026-07-23 22:56:30 -03:00
commit 22a1006598
1816 changed files with 410742 additions and 0 deletions
@@ -0,0 +1,124 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations;
class BricksBuilderIntegration
{
public function register()
{
add_filter('bricks/conditions/groups', [$this, 'addConditionGroup']);
add_filter('bricks/conditions/options', [$this, 'addConditionOptions']);
add_filter('bricks/conditions/result', [$this, 'checkCondition'], 10, 3);
}
public function addConditionGroup($groups)
{
// Ensure your group name is unique (best to prefix it)
$groups[] = [
'name' => 'fluent_crm',
'label' => esc_html__('FluentCRM', 'fluent-crm'),
];
return $groups;
}
public function addConditionOptions($options)
{
// Ensure key is unique, and that group exists
$tags = \FluentCrm\App\Models\Tag::select(['id', 'title'])->orderBy('title', 'ASC')->get();
$lists = \FluentCrm\App\Models\Lists::select(['id', 'title'])->orderBy('title', 'ASC')->get();
$formattedTags = [];
foreach ($tags as $tag) {
$formattedTags[$tag->id] = $tag->title;
}
$formattedLists = [];
foreach ($lists as $list) {
$formattedLists[$list->id] = $list->title;
}
$options[] = [
'key' => 'fluent_crm_tags',
'label' => esc_html__('FluentCRM Tags', 'fluent-crm'),
'group' => 'fluent_crm',
'compare' => [
'type' => 'select',
'options' => [
'==' => esc_html__('includes in', 'fluent-crm'),
'!=' => esc_html__('not includes', 'fluent-crm'),
],
'placeholder' => esc_html__('is', 'fluent-crm'),
],
'value' => [
'type' => 'select',
'multiple' => true,
'options' => $formattedTags,
'placeholder' => esc_html__('Select Tags', 'fluent-crm'),
],
];
$options[] = [
'key' => 'fluent_crm_lists',
'label' => esc_html__('FluentCRM Lists', 'fluent-crm'),
'group' => 'fluent_crm',
'compare' => [
'type' => 'select',
'options' => [
'==' => esc_html__('includes in', 'fluent-crm'),
'!=' => esc_html__('not includes', 'fluent-crm'),
],
'placeholder' => esc_html__('is', 'fluent-crm'),
],
'value' => [
'type' => 'select',
'multiple' => true,
'options' => $formattedLists,
'placeholder' => esc_html__('Select Lists', 'fluent-crm'),
],
];
return $options;
}
public function checkCondition($result, $condition_key, $condition)
{
$acceptedKeys = ['fluent_crm_tags', 'fluent_crm_lists'];
if (!in_array($condition_key, $acceptedKeys, true)) {
return $result;
}
// In my example, if compare is empty, we set it to '==' as default
$compare = \FluentCrm\Framework\Support\Arr::get($condition, 'compare', '==');
$targetIds = \FluentCrm\Framework\Support\Arr::get($condition, 'value', []);
$targetIds = array_filter(array_map('intval', $targetIds));
if (!$compare || !$targetIds) {
return true;
}
$currentContact = fluentcrm_get_current_contact();
if (!$currentContact) {
return $compare != '==';
}
if ($condition_key == 'fluent_crm_tags') {
$result = $currentContact->hasAnyTagId($targetIds);
} else {
$result = $currentContact->hasAnyListId($targetIds);
}
if ($compare == '==') {
return $result;
}
return !$result;
}
}
@@ -0,0 +1,136 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart\Benchmarks;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\CartHelper;
use FluentCrm\App\Services\Funnel\BaseBenchMark;
use FluentCrm\App\Services\Funnel\FunnelHelper;
use FluentCrm\App\Services\Funnel\FunnelProcessor;
use FluentCrm\Framework\Support\Arr;
class OrderSuccessBenchmark extends BaseBenchMark
{
public function __construct()
{
$this->triggerName = 'fluent_cart/order_paid_done';
$this->actionArgNum = 3;
$this->priority = 20;
parent::__construct();
}
public function getBlock()
{
return [
'title' => __('Order Paid (Payment/Subscription)', 'fluent-crm'),
'description' => __('This will run once new order will be placed as paid in FluentCRM', 'fluent-crm'),
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1"><g id="surface1"><path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 2.398438 0 L 21.601562 0 C 22.925781 0 24 1.074219 24 2.398438 L 24 21.601562 C 24 22.925781 22.925781 24 21.601562 24 L 2.398438 24 C 1.074219 24 0 22.925781 0 21.601562 L 0 2.398438 C 0 1.074219 1.074219 0 2.398438 0 Z M 2.398438 0 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 10.925781 16.476562 L 3.769531 16.476562 L 4.894531 13.878906 C 5.222656 13.117188 5.972656 12.625 6.804688 12.625 L 15.328125 12.625 L 14.746094 13.964844 C 14.085938 15.488281 12.585938 16.476562 10.925781 16.476562 Z M 10.925781 16.476562 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 16.851562 11.394531 L 6.789062 11.394531 L 7.367188 10.054688 C 8.027344 8.53125 9.53125 7.542969 11.191406 7.542969 L 19.886719 7.542969 L 18.761719 10.140625 C 18.433594 10.902344 17.683594 11.394531 16.851562 11.394531 Z M 16.851562 11.394531 "/></g></svg>',
'settings' => [
'product_ids' => [],
'product_categories' => [],
'type' => 'required',
'can_enter' => 'yes'
]
];
}
public function getDefaultSettings()
{
return [
'product_ids' => [],
'product_categories' => [],
'type' => 'required',
'can_enter' => 'yes'
];
}
public function getBlockFields($funnel)
{
return [
'title' => __('New Order Paid in FluentCart', 'fluent-crm'),
'sub_title' => __('This will run once new order will be placed as paid in FluentCart', 'fluent-crm'),
'fields' => [
'product_ids' => [
'type' => 'rest_selector',
'label' => __('Target Products', 'fluent-crm'),
'option_key' => 'fluent_cart_products',
'is_multiple' => true,
'help' => __('Select for which products this automation will run', 'fluent-crm'),
'inline_help' => __('Keep it blank to run to any product purchase', 'fluent-crm')
],
'product_categories' => [
'type' => 'rest_selector',
'label' => __('Or Target Product Categories', 'fluent-crm'),
'option_key' => 'fluent_cart_product_categories',
'is_multiple' => true,
'help' => __('Select for which product category the automation will run', 'fluent-crm'),
'inline_help' => __('Keep it blank to run to any category products', 'fluent-crm')
],
'type' => $this->benchmarkTypeField(),
'can_enter' => $this->canEnterField()
]
];
}
public function handle($benchMark, $originalArgs)
{
$orderData = $originalArgs[0] ?? [];
$order = Arr::get($orderData, 'order', []);
$customer = Arr::get($orderData, 'customer', []);
$settings = $benchMark->settings;;
if (!$this->checkConditions($settings, $order)) {
return;
}
$subscriberData = CartHelper::prepareSubscriberData($customer);
if (!is_email($subscriberData['email'])) {
return;
}
$subscriberData['status'] = 'subscribed';
$subscriber = FunnelHelper::createOrUpdateContact($subscriberData);
$funnelProcessor = new FunnelProcessor();
$funnelProcessor->startFunnelFromSequencePoint($benchMark, $subscriber, [], [
'benchmark_value' => $order->total_paid, // converted to cents
'benchmark_currency' => $order->currency,
]);
}
private function checkConditions($conditions, $order)
{
$orderItems = Arr::get($order, 'order_items', []);
// Post IDs of ordered products are the product IDs in FluentCart
$orderedProductIds = [];
foreach ($orderItems as $item) {
$productId = $item->post_id;
if ($productId) {
$orderedProductIds[] = $productId;
}
}
$orderProductCategories = CartHelper::getProductCategoriesByIds($orderedProductIds);
$selectedProductIds = Arr::get($conditions, 'product_ids', []);
$selectedProductCategories = Arr::get($conditions, 'product_categories', []);
// If no products or categories are selected, return true
if (empty($selectedProductIds) && empty($selectedProductCategories)) {
return true;
}
// Check for matches in product IDs and categories
$productMatch = !empty($selectedProductIds) && !empty(array_intersect($selectedProductIds, $orderedProductIds));
$categoryMatch = !empty($selectedProductCategories) && !empty(array_intersect($selectedProductCategories, $orderProductCategories));
// Return true if either matches
return $productMatch || $categoryMatch;
}
}
@@ -0,0 +1,220 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart;
use FluentCart\Api\Taxonomy;
use FluentCart\App\Helpers\Status;
use FluentCart\App\Models\Coupon;
use FluentCart\App\Models\Product;
use FluentCart\App\Models\ProductVariation;
use FluentCart\Framework\Database\Orm\Collection;
use FluentCart\Framework\Support\Str;
use FluentCart\App\Models\Customer;
class CartHelper
{
public static function getFluentCartProducts($items, $search, $ids = [])
{
$search = (string)$search;
$ids = is_array($ids) ? $ids : [];
try {
$productQuery = Product::query()->published();
if ($search) {
$productQuery->where('post_title', 'like', '%' . $search . '%');
}
$queried = $productQuery
->orderBy('post_title')
->limit(50)
->get(['ID', 'post_title']);
$options = [];
$pushedIds = [];
foreach ($queried as $product) {
$options[] = [
'id' => $product->ID,
'title' => $product->ID . '# ' . $product->post_title,
];
$pushedIds[] = $product->ID;
}
if ($ids) {
$remaining = array_diff($ids, $pushedIds);
if ($remaining) {
$extraProducts = Product::query()->published()->whereIn('ID', $remaining)->get(['ID', 'post_title']);
foreach ($extraProducts as $product) {
$options[] = [
'id' => $product->ID,
'title' => $product->ID . '# ' . $product->post_title,
];
}
}
}
return $options;
} catch (\Exception $e) {
return [];
}
}
public static function getFluentCartCoupons($items, $search, $ids)
{
try {
$coupons = Coupon::all(['id', 'title'])
->map(function ($coupon) {
return [
'id' => $coupon->id,
'title' => $coupon->title,
];
})
->toArray();
} catch (\Exception $e) {
$coupons = [];
}
return $coupons;
}
public static function getFluentCartProductCategories($items, $search, $ids)
{
try {
$taxonomies = Taxonomy::getTaxonomies();
$taxonomies = Collection::make($taxonomies)
->map(function ($taxonomy) {
return [
'name' => $taxonomy,
'label' => Str::headline($taxonomy),
'terms' => Taxonomy::getFormattedTerms($taxonomy),
];
});
$categories = Collection::make($taxonomies['product-categories']['terms'])
->map(function ($term) {
return [
'id' => $term['value'],
'title' => $term['label'],
];
})
->toArray();
} catch (\Exception $e) {
$categories = [];
}
return $categories;
}
public static function getProductCategoriesByIds($ids)
{
try {
$products = Product::with('wp_terms')->whereIn('id', $ids)->get();
$categories = $products->flatMap(function ($product) {
return $product->wp_terms->pluck('term_taxonomy_id');
})->unique()->values()->toArray();
} catch (\Exception $e) {
$categories = [];
}
return $categories;
}
public static function getFluentCartSubscriptionProducts($items, $search, $ids)
{
$search = (string)$search;
$ids = is_array($ids) ? $ids : [];
try {
$variationQuery = ProductVariation::query()
->where('payment_type', 'subscription')
->where('item_status', 'active');
if ($search) {
$variationQuery->where('variation_title', 'like', '%' . $search . '%');
}
$productIds = $variationQuery->pluck('post_id')->unique()->slice(0, 50)->values();
$pushedIds = $productIds->toArray();
if ($ids) {
$appendIds = array_diff($ids, $pushedIds);
if ($appendIds) {
$productIds = $productIds->merge($appendIds);
}
}
if ($productIds->isEmpty()) {
return [];
}
$products = Product::query()
->published()
->whereIn('ID', $productIds->toArray())
->orderBy('post_title')
->get(['ID', 'post_title']);
$formatted = [];
foreach ($products as $product) {
$formatted[] = [
'id' => $product->ID,
'title' => $product->ID . '# ' . $product->post_title,
];
}
return $formatted;
} catch (\Exception $e) {
return [];
}
}
public static function prepareSubscriberData($customer)
{
if(!is_object($customer)) {
$customer = (object) $customer;
}
return [
'email' => $customer->email,
'first_name' => $customer->first_name,
'last_name' => $customer->last_name,
'full_name' => $customer->first_name . ' ' . $customer->last_name,
'user_id' => $customer->user_id,
'postal_code' => $customer->postcode,
'country' => $customer->country,
'state' => $customer->state,
'city' => $customer->city,
'phone' => $customer->phone,
];
}
public static function getCustomersByProductIds($productIds, $offset = 0, $limit = 100)
{
$customers = [];
try {
$customers = Customer::query()->whereHas('success_order_items', function ($q) use ($productIds) {
$q->whereIn('post_id', $productIds);
})->offset($offset)->limit($limit)->get();
} catch (\Exception $e) {
}
return $customers;
}
public static function getPurchasedProductsByCustomerId($customerId)
{
$productIds = [];
try {
$orderIds = fluentCrmDb()->table('fct_orders')
->where('customer_id', $customerId)
->pluck('id');
$productIds = fluentCrmDb()->table('fct_order_items')
->whereIn('order_id', $orderIds)
->pluck('post_id');
} catch (\Exception $e) {
}
return $productIds;
}
}
@@ -0,0 +1,332 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart;
use Automattic\WooCommerce\Blocks\BlockTypes\Cart;
use FluentCrm\App\Models\Subscriber;
use FluentCrm\App\Models\Tag;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\App\Services\Helper;
class CartImporter
{
// Customers processed per request (page size)
const PER_PAGE = 150;
// public function __construct()
// {
// $this->importKey = 'fluent_cart';
// parent::__construct();
// }
private static function getPluginName()
{
return 'FluentCart';
}
// public function getInfo()
// {
// return [
// 'label' => $this->getPluginName(),
// 'logo' => fluentCrmMix('images/woo.svg'),
// 'disabled' => false
// ];
// }
public static function processUserDriver($config, $request)
{
$summary = $request->get('summary');
if ($summary) {
// Initialize defaults to avoid undefined variables in fallback returns
$formattedCustomers = [];
$total = 0;
$config = $request->get('config');
$type = Arr::get($config, 'import_type');
if ($type == 'customers_sync') {
// $customersQuery = fluentCrmDb()->table('wc_customer_lookup');
// $total = $customersQuery->count();
// $formattedUsers = $customersQuery->select(['first_name', 'last_name', 'email'])->limit(5)->get();
// foreach ($formattedUsers as $formattedUser) {
// $formattedUser->name = trim($formattedUser->first_name . ' ' . $formattedUser->last_name);
// }
} else if ($type == 'product_tags') {
$productIds = [];
foreach ($config['product_type_maps'] as $map) {
$productIds[] = absint($map['field_key']);
}
// get customers who purchased these products from fluent cart
// get orderIds from
$customers = CartHelper::getCustomersByProductIds($productIds, 0, 100000);
$total = count($customers);
fluentcrm_update_option('_fluent_cart_product_tag_total_count', $total);
$formattedCustomers = [];
foreach ($customers as $customer) {
$formattedCustomers[] = [
'name' => $customer->first_name . ' ' . $customer->last_name,
'email' => $customer->email
];
}
return [
'import_info' => [
'subscribers' => $formattedCustomers,
'total' => $total,
'has_tag_config' => false,
'has_list_config' => true,
'has_status_config' => false,
'has_update_config' => false,
'has_silent_config' => true
]
];
}
return [
'import_info' => [
'subscribers' => $formattedCustomers,
'total' => $total,
'has_tag_config' => true,
'has_list_config' => true,
'has_status_config' => true,
'has_update_config' => false,
'has_silent_config' => true
]
];
}
$importType = 'customers_sync';
/* translators: %s: the external commerce plugin name */
$importTitle = sprintf(__('Sync %s Customers Now', 'fluent-crm'), self::getPluginName());
if(defined('FLUENTCART_VERSION')) {
$importType = 'product_tags';
/* translators: %s: the external commerce plugin name */
$importTitle = sprintf(__('Import %s Customers Now', 'fluent-crm'), self::getPluginName());
}
$configFields = [
'config' => [
'import_type' => $importType,
'product_type_maps' => [
[
'field_key' => '',
'field_value' => ''
]
]
],
'fields' => [
'product_type_maps' => [
'label' => __('Please map your Product and associate FluentCRM Tags', 'fluent-crm'),
'type' => 'form-many-drop-down-mapper',
/* translators: %s: the external commerce plugin name */
'local_label' => sprintf(__('Select %s Product', 'fluent-crm'), self::getPluginName()),
'remote_label' => __('Select FluentCRM Tag that will be applied', 'fluent-crm'),
/* translators: %s: the external commerce plugin name */
'local_placeholder' => sprintf(__('Select %s Product', 'fluent-crm'), self::getPluginName()),
'remote_placeholder' => __('Select FluentCRM Tag', 'fluent-crm'),
'field_ajax_selector' => [
'option_key' => 'fluent_cart_products'
],
'value_option_selector' => [
'option_key' => 'tags',
'creatable' => true
],
'dependency' => [
'depends_on' => 'import_type',
'operator' => '=',
'value' => 'product_tags'
]
],
'sync_import_html' => [
'type' => 'html-viewer',
'heading' => __('FluentCart Data Sync', 'fluent-crm'),
'info' => __('You can sync all your FluentCart Customers into FluentCRM and all future customers and purchase data will be synced.', 'fluent-crm').'<br />'.__('After this sync you can import by product by product and provide appropriate tags', 'fluent-crm'),
'dependency' => [
'depends_on' => 'import_type',
'operator' => '=',
'value' => 'customers_sync'
]
]
],
'labels' => [
'step_2' => __('Next [Review Data]', 'fluent-crm'),
'step_3' => $importTitle
]
];
return $configFields;
}
public static function importData($returnData, $config, $page)
{
$inputs = Arr::only($config, [
'lists', 'tags', 'status', 'double_optin_email', 'import_silently'
]);
$inputs = wp_parse_args($inputs, [
'lists' => [],
'tags' => [],
// keep backward compatibility but use `status` key everywhere
'status' => 'subscribed',
'double_optin_email' => 'no',
'import_silently' => 'yes'
]);
if (Arr::get($inputs, 'import_silently') == 'yes') {
if (!defined('FLUENTCRM_DISABLE_TAG_LIST_EVENTS')) {
define('FLUENTCRM_DISABLE_TAG_LIST_EVENTS', true);
}
}
$sendDoubleOptin = Arr::get($inputs, 'double_optin_email') == 'yes';
$contactStatus = Arr::get($inputs, 'status', 'subscribed');
$productTagMaps = [];
$productIds = [];
foreach ($config['product_type_maps'] as $map) {
$productId = absint($map['field_key']);
$productIds[] = $productId;
if (!isset($productTagMaps[$productId])) {
$productTagMaps[$productId] = [];
}
$productTagMaps[$productId][] = absint($map['field_value']);
}
$productIds = array_unique($productIds);
$startTime = time();
$runTime = 20; // seconds
// normalize and initialize paging
$perPage = self::PER_PAGE;
$page = max(1, absint($page));
// reset counters on first page to start a fresh run
if ($page === 1) {
fluentcrm_update_option('_fluent_cart_sync_count', 0);
fluentcrm_update_option('_fluent_cart_import_current_page', 1);
}
$customers = CartHelper::getCustomersByProductIds($productIds, ($page - 1) * $perPage, $perPage);
// Even if the current page has no customers (e.g., last sparse page),
// advance the page pointer to avoid getting stuck reprocessing the same page.
if (empty($customers)) {
fluentcrm_update_option('_fluent_cart_import_current_page', $page);
return self::getSyncStatus();
}
$importedCustomers = [];
// pushing all customers without tags
foreach ($customers as $customer) {
$subscribers = CartHelper::prepareSubscriberData($customer);
if($customer->user_id) {
$subscribers = Helper::getWPMapUserInfo($customer->user_id);
}
Subscriber::import(
[$subscribers],
[],
Arr::get($inputs, 'lists', []),
true,
$contactStatus,
$sendDoubleOptin
);
// keeping track of imported customers
$purchasedProducts = self::purchasedProductsOfImportedCustomer($customer->id, $productIds);
$assignedTags = self::mapCustomerTags($purchasedProducts, $productTagMaps);
$importedCustomers[] = [
'customer_id' => $customer->id,
'email' => $subscribers['email'],
'user_id' => $customer->user_id,
'purchased_products' => $purchasedProducts,
'tags' => $assignedTags
];
// update tags for the subscriber
if ($assignedTags) {
$importedSubscriber = Subscriber::where('email', $subscribers['email'])->first();
if ($importedSubscriber) {
$importedSubscriber->attachTags($assignedTags);
}
}
}
$totalSynced = count($importedCustomers) + (($page - 1) * $perPage);
fluentcrm_update_option('_fluent_cart_sync_count', $totalSynced);
fluentcrm_update_option('_fluent_cart_import_current_page', $page);
// check time limit
if (time() - $startTime > $runTime) {
return self::getSyncStatus();
}
return self::getSyncStatus();
}
private static function mapCustomerTags($purchasedProducts, $productTagMaps)
{
$assignedTags = [];
foreach ($purchasedProducts as $purchasedProduct) {
if (isset($productTagMaps[$purchasedProduct])) {
$assignedTags = array_merge($productTagMaps[$purchasedProduct], $assignedTags); //
}
}
return array_values(array_unique($assignedTags)); // return only unique tags
}
private static function purchasedProductsOfImportedCustomer($customerId, $productIds)
{
$allProductsIds = CartHelper::getPurchasedProductsByCustomerId($customerId);
// check in products
// getPurchasedProductsByCustomerId() returns a Collection via pluck(), convert to array for array_intersect()
return array_intersect($allProductsIds->toArray(), $productIds); // return only those products which are in productIds
}
private static function getSyncStatus()
{
$total = fluentcrm_get_option('_fluent_cart_product_tag_total_count', 0);
$perPage = self::PER_PAGE;
// Calculate page-based progress for the frontend
$totalPages = max(1, (int) ceil($total / max(1, $perPage)));
$currentPage = (int) fluentcrm_get_option('_fluent_cart_import_current_page', 1);
$currentPage = max(1, min($currentPage, $totalPages));
$hasMore = $currentPage < $totalPages;
return [
// Total number of pages to import
'page_total' => $totalPages,
// Total records for reference
'record_total' => $total,
'has_more' => $hasMore,
'current_page' => $currentPage,
'next_page' => $hasMore ? ($currentPage + 1) : 0,
'reload_page' => !$hasMore
];
}
}
@@ -0,0 +1,211 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart;
use FluentCart\App\Helpers\AddressHelper;
use FluentCrm\App\Services\AutoSubscribe;
use FluentCrm\App\Services\Funnel\FunnelHelper;
use FluentCrm\Framework\Support\Arr;
/**
* Adds a newsletter opt-in checkbox to the FluentCart checkout form and
* subscribes the customer to FluentCRM when the order is paid.
*
* Controlled by the 'FluentCart Checkout Subscription Field' panel in
* FluentCRM -> Settings -> General Settings (option key:
* fluent_cart_checkout_form_subscribe_settings, defined in the base plugin's
* AutoSubscribe service).
*/
class CheckoutSubscription
{
/**
* Form field name posted with the checkout request and the
* order meta key holding the captured opt-in state.
*/
const OPTIN_FIELD = '_fc_cart_checkout_subscribe';
/**
* Order meta flag preventing the same order from being processed twice.
*/
const PROCESSED_META = '_fc_cart_checkout_optin_processed';
public function init()
{
// Renders inside the checkout <form> on both the standard checkout
// page and the modal checkout (FormData picks the value up on submit)
add_action('fluent_cart/before_payment_methods', [$this, 'renderOptinCheckbox']);
// Fires while the order is being created from the checkout request —
// the only point where the raw POST data is available
add_action('fluent_cart/checkout/prepare_other_data', [$this, 'captureOptinState']);
// Async (Action Scheduler) hook recommended by FluentCart for
// third-party post-payment processing
add_action('fluent_cart/order_paid_done', [$this, 'maybeSubscribeContact'], 20);
}
protected function getSettings()
{
return (new AutoSubscribe())->getFluentCartCheckoutSettings();
}
/**
* Prints the opt-in checkbox above the payment methods section,
* using FluentCart's native checkbox markup classes.
*/
public function renderOptinCheckbox($data)
{
$settings = $this->getSettings();
if (Arr::get($settings, 'status') != 'yes') {
return;
}
if (Arr::get($settings, 'show_only_new') == 'yes') {
$contact = fluentcrm_get_current_contact();
if ($contact && $contact->status == 'subscribed') {
return;
}
}
$label = Arr::get($settings, 'checkbox_label');
if (!$label) {
$label = __('Sign me up for the newsletter!', 'fluent-crm');
}
$isChecked = Arr::get($settings, 'auto_checked') == 'yes';
?>
<div class="fct_checkout_form_section fcrm_checkout_subscribe" data-fct-checkout-form-section>
<div class="fct_form_section_body">
<label class="fct_input_label fct_input_label_checkbox" for="fcrm_cart_subscribe">
<input type="checkbox" class="fct-input fct-input-checkbox" id="fcrm_cart_subscribe"
name="<?php echo esc_attr(self::OPTIN_FIELD); ?>" value="1" <?php checked($isChecked); ?>>
<?php echo esc_html($label); ?>
</label>
</div>
</div>
<?php
}
/**
* Persists the posted opt-in state to order meta so the async
* order-paid handler can read it later. Missing value = unchecked.
*/
public function captureOptinState($eventData)
{
$order = Arr::get($eventData, 'order');
if (!$order || !is_object($order)) {
return;
}
$settings = $this->getSettings();
if (Arr::get($settings, 'status') != 'yes') {
return;
}
$isChecked = Arr::get((array)Arr::get($eventData, 'request_data', []), self::OPTIN_FIELD) == '1';
$order->updateMeta(self::OPTIN_FIELD, $isChecked ? '1' : '0');
}
/**
* Creates/updates the FluentCRM contact when a paid order has the
* opt-in flag. Runs async via fluent_cart/order_paid_done. Idempotent —
* payment retries or hook re-runs will not duplicate processing.
*/
public function maybeSubscribeContact($eventData)
{
$order = Arr::get($eventData, 'order');
$customer = Arr::get($eventData, 'customer');
if (!$order || !$customer) {
return false;
}
// Treat both the final marker and an in-flight claim as processed so
// serial retries of the paid-order event (e.g. Action Scheduler
// re-runs) cannot double-process. This read-then-write guard is not
// atomic, so it does not protect against truly concurrent invocations
$processedState = $order->getMeta(self::PROCESSED_META);
if ($processedState == 'yes' || $processedState == 'processing') {
return false;
}
if ($order->getMeta(self::OPTIN_FIELD) != '1') {
return false;
}
$settings = $this->getSettings();
if (Arr::get($settings, 'status') != 'yes') {
return false;
}
$email = sanitize_email($customer->email);
if (!$email || !is_email($email)) {
return false;
}
$orderBillingAddress = $order->billing_address;
$address1 = Arr::get($orderBillingAddress, 'address_1', '');
$address2 = Arr::get($orderBillingAddress, 'address_2', '');
$state = AddressHelper::getStateNameByCode($customer->state, $customer->country);
$subscriberData = [
'first_name' => sanitize_text_field($customer->first_name),
'last_name' => sanitize_text_field($customer->last_name),
'country' => sanitize_text_field($customer->country),
'state' => sanitize_text_field($state),
'city' => sanitize_text_field($customer->city),
'postal_code' => sanitize_text_field($customer->postcode),
'address_line_1' => sanitize_text_field($address1),
'address_line_2' => sanitize_text_field($address2),
'email' => $email
];
if ($listId = Arr::get($settings, 'target_list')) {
$subscriberData['lists'] = [$listId];
}
if ($tags = Arr::get($settings, 'target_tags')) {
$subscriberData['tags'] = $tags;
}
if (Arr::get($settings, 'double_optin') == 'yes') {
$subscriberData['status'] = 'pending';
} else {
$subscriberData['status'] = 'subscribed';
}
$subscriberData = apply_filters('fluent_crm/fluent_cart_checkout_auto_subscribe_data', $subscriberData, $order);
// Claim the order before contact creation / email side effects so an
// Action Scheduler retry re-entering this handler cannot run them twice
$order->updateMeta(self::PROCESSED_META, 'processing');
try {
$contact = FunnelHelper::createOrUpdateContact($subscriberData);
if (!$contact) {
// Release the claim so a later retry can attempt processing again
$order->updateMeta(self::PROCESSED_META, '0');
return false;
}
if ($contact->status == 'pending') {
$contact->sendDoubleOptinEmail();
}
} catch (\Throwable $e) {
// Release the claim on failure too — otherwise the order would be
// stuck in 'processing' and permanently skipped by the guard above
$order->updateMeta(self::PROCESSED_META, '0');
throw $e;
}
$order->updateMeta(self::PROCESSED_META, 'yes');
return true;
}
}
@@ -0,0 +1,508 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart;
use FluentCart\Api\ModuleSettings;
use FluentCart\App\Helpers\Helper;
use FluentCart\App\Models\Customer;
use FluentCart\App\Models\Order;
use FluentCrm\App\Models\Subscriber;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\Benchmarks\OrderSuccessBenchmark;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\SmartCode\SmartCodeParser;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\SmartCode\SmartCodeRegister;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers\OrderCanceledTrigger;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers\OrderPaidTrigger;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers\OrderDeliveredTrigger;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers\OrderRefundedTrigger;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers\OrderShippedTrigger;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers\OrderStatusChangedTrigger;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers\SubscriptionActivatedTrigger;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers\SubscriptionCancelledTrigger;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers\SubscriptionEndOfTermTrigger;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers\SubscriptionExpiredTrigger;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers\SubscriptionRenewedTrigger;
class FluentCart
{
public function init()
{
$this->addAutomations();
$this->addHooks();
SmartCodeRegister::push();
(new RevenueTracker())->init();
}
public function addAutomations()
{
new OrderPaidTrigger();
new OrderShippedTrigger();
new OrderDeliveredTrigger();
new OrderRefundedTrigger();
new OrderCanceledTrigger();
// new OrderStatusChangedTrigger();
new SubscriptionExpiredTrigger();
//subscription activated
new SubscriptionActivatedTrigger();
//subscription cancelled
new SubscriptionCancelledTrigger();
//subscription renewed
new SubscriptionRenewedTrigger();
//subscription end of term(completed)
new SubscriptionEndOfTermTrigger();
// Goals
new OrderSuccessBenchmark();
}
public function addHooks()
{
add_filter('fluent_crm/get_import_driver_fluent_cart', [CartImporter::class, 'processUserDriver'], 10, 2);
add_filter('fluent_crm/post_import_driver_fluent_cart', [CartImporter::class, 'importData'], 10, 3);
add_filter('fluentcrm_ajax_options_fluent_cart_products', [$this, 'getProducts'], 10, 3);
add_filter('fluentcrm_ajax_options_fluent_cart_product_categories', [$this, 'getProductCategories'], 10, 3);
add_filter('fluentcrm_ajax_options_fluent_cart_subscription_products', [$this, 'getSubscriptionProducts'], 10, 3);
add_filter('fluent_crm/funnel_icons', [$this, 'addCartIcon'], 10 , 1);
add_filter('fluent_crm/purchase_history_fluent_cart', [$this, 'purchaseHistory'], 10, 2);
add_filter('fluent_crm/smartcode_group_callback_cart_order', [SmartCodeParser::class, 'parseCartOrder'], 10, 4);
add_filter('fluent_crm/smartcode_group_callback_cart_customer', [SmartCodeParser::class, 'parseCartCustomer'], 10, 4);
// add_filter('fluent_crm/smartcode_group_callback_cart_transaction', [SmartCodeParser::class, 'parseCartTransaction'], 10, 4);
add_filter('fluent_crm/smartcode_group_callback_cart_receipt', [SmartCodeParser::class, 'parseCartReceipt'], 10, 4);
add_filter('fluentcrm_automation_condition_groups', array($this, 'addAutomationConditions'), 10, 2);
add_filter('fluentcrm_automation_conditions_assess_fluent_cart', array($this, 'assessAutomationConditions'), 10, 3);
// add_filter('fluentcrm_automation_conditions_assess_woo_order', array($this, 'assessAutomationOrderConditions'), 10, 5);
}
public function getProducts($items, $search, $ids)
{
return CartHelper::getFluentCartProducts($items, $search, $ids);
}
public function getProductCategories($items, $search, $ids)
{
return CartHelper::getFluentCartProductCategories($items, $search, $ids);
}
public function getSubscriptionProducts($items, $search, $ids)
{
return CartHelper::getFluentCartSubscriptionProducts($items, $search, $ids);
}
public function addCartIcon($icons)
{
$icons['fluentcart'] = [
'svg' => '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="18" height="18" rx="1.8" fill="#00009F"/><path d="M9.19408 13.3557H3.82861L4.67063 11.4086C4.91784 10.8369 5.48112 10.4668 6.10395 10.4668H12.4955L12.0607 11.4722C11.5663 12.6155 10.4397 13.3557 9.19408 13.3557Z" fill="white"/><path d="M13.6389 9.54518H6.0918L6.52656 8.5398C7.02098 7.39646 8.14753 6.65625 9.39319 6.65625H15.9142L15.0722 8.60341C14.825 9.17507 14.2617 9.54518 13.6389 9.54518Z" fill="white"/></svg>',
];
return $icons;
}
public function purchaseHistory($data, $subscriber)
{
$customer = Customer::where('email', $subscriber->email)->first();
if (!$customer) {
return [];
}
$ordersQuery = Order::with('appliedCoupons')->where('customer_id', $customer->id);
$totalCount = $ordersQuery->count();
if (!$totalCount) {
return [];
}
// Pagination params (using super global to avoid dependency on request wrapper here)
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($page < 1) { $page = 1; }
$perPage = isset($_GET['per_page']) ? (int)$_GET['per_page'] : 10;
if ($perPage < 1) { $perPage = 10; }
$orders = $ordersQuery
->orderBy('id', 'DESC')
->limit($perPage)
->offset(($page - 1) * $perPage)
->get();
// Use a helper method for formatting
$formattedOrders = $this->formatOrders($orders);
return [
'data' => $formattedOrders,
'total' => $totalCount,
'sidebar_html' => $this->getSidebarHtml($subscriber),
'after_html' => '',
'has_recount' => false,
'columns_config' => [
'order' => ['label' => __('Order', 'fluent-crm'), 'width' => '100px', 'sortable' => true, 'key' => 'id'],
'date' => ['label' => __('Date', 'fluent-crm'), 'sortable' => true, 'key' => 'created_at'],
// 'coupon' => ['label' => __('Coupon', 'fluent-crm'), 'sortable' => true, 'key' => 'created_at'],
'status' => ['label' => __('Status', 'fluent-crm'), 'width' => '140px', 'sortable' => false],
// 'payment' => ['label' => __('Payment', 'fluent-crm'), 'width' => '140px', 'sortable' => false, 'key' => 'payment_status'],
'total' => ['label' => __('Total', 'fluent-crm'), 'width' => '120px', 'sortable' => true, 'key' => 'total'],
'action' => ['label' => __('', 'fluent-crm'), 'width' => '100px', 'sortable' => false],
],
];
}
private function formatOrders($orders)
{
$formattedOrders = [];
foreach ($orders as $order) {
$orderActionHtml = '<a target="_blank" href="' . admin_url('admin.php?page=fluent-cart#/orders/' . $order->id . '/view') . '">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 5.5V7H4.75V15.25H13V11.5H14.5V16C14.5 16.1989 14.421 16.3897 14.2803 16.5303C14.1397 16.671 13.9489 16.75 13.75 16.75H4C3.80109 16.75 3.61032 16.671 3.46967 16.5303C3.32902 16.3897 3.25 16.1989 3.25 16V6.25C3.25 6.05109 3.32902 5.86032 3.46967 5.71967C3.61032 5.57902 3.80109 5.5 4 5.5H8.5ZM16.75 3.25V9.25H15.25V5.80975L9.40525 11.6553L8.34475 10.5948L14.1888 4.75H10.75V3.25H16.75Z" fill="#525866"/>
</svg>
</a>';
$coupons = implode(', ', array_column($order['appliedCoupons']->toArray(), 'code'));
$date = '<span class="order_id">#' . $order->id .'</span><span class="order_date">'.date_i18n(get_option('date_format'), strtotime($order->created_at)).'</span>';
$status = '<span class="fcrm_badge fcrm_badge_'.esc_attr($order->status).'">'. \FluentCrm\App\Services\Helper::getStatusText($order->status) .'</span>';
$formattedOrders[] = [
'date' => $date,
'status' => $status,
// 'payment' => $order->payment_status,
// 'coupons' => $coupons,
'total' => Helper::toDecimal($order->total_amount), // Adjust if using a different helper
'action' => $orderActionHtml,
];
}
return $formattedOrders;
}
private function getSidebarHtml($subscriber = null)
{
// We will build a similar widget like WooCommerce's purchase sidebar
// Show a quick customer summary + recently purchased products (flat list of items)
$customer = null;
if ($subscriber && !empty($subscriber->email)) {
$customer = Customer::where('email', $subscriber->email)->first();
}
if (!$customer) {
// Fallback: Just return the static block as before
return '<div class="fluent-crm-sidebar-content">'
. '<h3>' . __('Product History', 'fluent-crm') . '</h3>'
. '<p>' . __('View your purchase history from FluentCart.', 'fluent-crm') . '</p>'
. '</div>';
}
// Aggregate order stats
$ordersQuery = Order::where('customer_id', $customer->id);
$orderCount = (clone $ordersQuery)->count();
if (!$orderCount) {
return '<div class="fluent-crm-sidebar-content">'
. '<h3>' . __('Product History', 'fluent-crm') . '</h3>'
. '<p>' . __('No purchases found for this contact in FluentCart.', 'fluent-crm') . '</p>'
. '</div>';
}
$totalSpent = (clone $ordersQuery)->sum('total_amount');
$firstOrder = (clone $ordersQuery)->orderBy('id', 'ASC')->value('created_at');
$lastOrder = (clone $ordersQuery)->orderBy('id', 'DESC')->value('created_at');
// Fetch recent purchased items (latest 15 order items across latest orders)
// We'll eager load order_items for performance
$recentOrders = (clone $ordersQuery)
->with(['order_items' => function($q){
$q->orderBy('id', 'DESC');
}])
->orderBy('id', 'DESC')
->limit(200)
->get();
$items = [];
foreach ($recentOrders as $order) {
foreach ($order->order_items as $orderItem) {
$itemDisplayName = $orderItem->post_title;
if($orderItem->post_title != $orderItem->title) {
$itemDisplayName = $orderItem->post_title . ' - ' . $orderItem->title;
}
$items[] = [
'name' => $itemDisplayName,
'price' => isset($orderItem->line_total) ? Helper::toDecimal($orderItem->line_total) : 0,
'created_at' => $order->created_at,
'order_id' => $order->id,
];
if (count($items) >= 15) {
break 2; // Exit both loops once we have enough
}
}
}
$html = '<div class="fluent-crm-sidebar-content fc_payment_summary">';
$html .= '<h3 class="history_title">' . __('Order Summary', 'fluent-crm') . '</h3>';
$html .= '<div class="fc_history_widget"><ul class="fc_full_listed">';
$html .= '<li><span class="fc_list_sub">' . __('Total Orders', 'fluent-crm') . '</span><span class="fc_list_value">' . intval($orderCount) . '</span></li>';
$html .= '<li><span class="fc_list_sub">' . __('Total Spent', 'fluent-crm') . '</span><span class="fc_list_value">' . esc_html(Helper::toDecimal($totalSpent)) . '</span></li>';
if ($firstOrder) {
$html .= '<li><span class="fc_list_sub">' . __('First Order', 'fluent-crm') . '</span><span class="fc_list_value">' . date_i18n(get_option('date_format'), strtotime($firstOrder)) . '</span></li>';
}
if ($lastOrder) {
$html .= '<li><span class="fc_list_sub">' . __('Last Order', 'fluent-crm') . '</span><span class="fc_list_value">' . date_i18n(get_option('date_format'), strtotime($lastOrder)) . '</span></li>';
}
$html .= '</ul></div>';
$html .= '<h3 class="history_title">' . __('Purchased Products', 'fluent-crm') . '</h3>';
$html .= '<div class="fc_history_widget"><ul class="fc_full_listed max_height_550">';
foreach ($items as $item) {
$orderUrl = admin_url('admin.php?page=fluent-cart#/orders/' . $item['order_id'] . '/view');
$badges = '<span class="el-tag el-tag--primary">' . esc_html(Helper::toDecimal($item['price'])) . '</span>';
$badges .= '<span class="el-tag el-tag--primary"><a target="_blank" rel="noopener" href="' . esc_url($orderUrl) . '">' . date_i18n(get_option('date_format'), strtotime($item['created_at'])) . '</a></span>';
$html .= '<li class="fc_product_name">' . esc_html($item['name']) . ' ' . $badges . '</li>';
}
if (!$items) {
$html .= '<li>' . __('No purchased products found.', 'fluent-crm') . '</li>';
}
$html .= '</ul></div>';
$html .= '</div>';
return $html;
}
public function addAutomationConditions($groups)
{
$conditionItems = [
[
'value' => 'commerce_exist',
'label' => __('Is a customer?', 'fluent-crm'),
'type' => 'selections',
'is_multiple' => false,
'disable_values' => true,
'value_description' => __('This filter will check if a contact has at least one shop order or not', 'fluent-crm'),
'custom_operators' => [
'exist' => __('Yes', 'fluent-crm'),
'not_exist' => __('No', 'fluent-crm'),
]
],
[
'value' => 'ltv',
'label' => __('Lifetime Value', 'fluent-crm'),
'type' => 'numeric'
],
[
'value' => 'aov',
'label' => __('Average Order Value', 'fluent-crm'),
'type' => 'numeric',
],
[
'value' => 'first_purchase_date',
'label' => __('First Order Date', 'fluent-crm'),
'type' => 'dates'
],
[
'value' => 'last_purchase_date',
'label' => __('Last Order Date', 'fluent-crm'),
'type' => 'dates'
],
[
'value' => 'purchased_items',
'label' => __('Products', 'fluent-crm'),
'type' => 'selections',
'component' => 'product_selector',
'is_multiple' => true,
'custom_operators' => [
'exist' => __('purchased', 'fluent-crm'),
'not_exist' => __('not purchased', 'fluent-crm'),
],
'help' => __('Will filter the contacts who have at least one order', 'fluent-crm')
],
[
'value' => 'variation_purchased',
'label' => __('Product Variations', 'fluent-crm'),
'type' => 'cascade_selections',
'provider' => 'fct_variations',
'is_multiple' => true,
'value_description' => __('This filter will check if a contact has purchased at least one specific product variation or not', 'fluent-crm'),
'custom_operators' => [
'exist' => __('purchased', 'fluent-crm'),
'not_exist' => __('not purchased', 'fluent-crm'),
]
],
[
'value' => 'purchased_categories',
'label' => __('Product Categories', 'fluent-crm'),
'type' => 'selections',
'component' => 'tax_selector',
'taxonomy' => 'product-categories',
'is_multiple' => true,
'disabled' => true,
'help' => __('Will filter the contacts who have at least one order', 'fluent-crm'),
'custom_operators' => [
'exist' => __('purchased', 'fluent-crm'),
'not_exist' => __('not purchased', 'fluent-crm'),
]
],
[
'value' => 'commerce_coupons',
'label' => __('Used Coupons', 'fluent-crm'),
'type' => 'selections',
'component' => 'ajax_selector',
'option_key' => 'fct_coupons',
'is_multiple' => true,
'disabled' => true,
'custom_operators' => [
'exist' => __('in', 'fluent-crm'),
'not_exist' => __('not in', 'fluent-crm'),
],
'help' => __('Will filter the contacts who have at least one order', 'fluent-crm')
]
];
if (ModuleSettings::isActive('license')) {
$conditionItems[] = [
'value' => 'active_licenses',
'label' => __('Active Licenses', 'fluent-crm'),
'type' => 'selections',
'component' => 'product_selector',
'is_multiple' => true,
'custom_operators' => [
'exist' => __('have', 'fluent-crm'),
'not_exist' => __('do not have', 'fluent-crm'),
],
'help' => __('Will filter the contacts who have at least one active licenses or not', 'fluent-crm')
];
$conditionItems[] = [
'value' => 'active_variation_licenses',
'label' => __('Active Variation Licenses', 'fluent-crm'),
'type' => 'cascade_selections',
'provider' => 'fct_variations',
'is_multiple' => true,
'value_description' => __('This filter will check if a contact has at least one specific variation license or not', 'fluent-crm'),
'custom_operators' => [
'exist' => __('have', 'fluent-crm'),
'not_exist' => __('do not have', 'fluent-crm'),
]
];
$conditionItems[] = [
'value' => 'expired_licenses',
'label' => __('Expired Licenses', 'fluent-crm'),
'type' => 'selections',
'component' => 'product_selector',
'is_multiple' => true,
'custom_operators' => [
'exist' => __('have', 'fluent-crm'),
'not_exist' => __('do not have', 'fluent-crm'),
],
'help' => __('Will filter the contacts who have at least one expired licenses or not', 'fluent-crm')
];
$conditionItems[] = [
'value' => 'expired_variation_licenses',
'label' => __('Expired Variation Licenses', 'fluent-crm'),
'type' => 'cascade_selections',
'provider' => 'fct_variations',
'is_multiple' => true,
'value_description' => __('This filter will check if a contact has at least one specific variation expired license or not', 'fluent-crm'),
'custom_operators' => [
'exist' => __('have', 'fluent-crm'),
'not_exist' => __('do not have', 'fluent-crm'),
]
];
$conditionItems[] = [
'value' => 'license_exist',
'label' => __('Has any active license?', 'fluent-crm'),
'type' => 'selections',
'is_multiple' => false,
'disable_values' => true,
'value_description' => __('Check if contacts has any active license from any products', 'fluent-crm'),
'custom_operators' => [
'exist' => __('Yes', 'fluent-crm'),
'not_exist' => __('No', 'fluent-crm'),
]
];
}
$groups['fluent_cart'] = [
'label' => __('FluentCart', 'fluent-crm'),
'value' => 'fluent_cart',
'children' => $conditionItems
];
return $groups;
}
public function assessAutomationConditions($result, $conditions, $subscriber)
{
$legacyConditions = [];
// if (Commerce::isEnabled('woo')) {
$formattedConditions = [];
$commerceProps = [
'commerce_exist',
'ltv', // lifetime value
'aov', // average order value
'first_purchase_date',
'last_purchase_date',
'purchased_items', // products purchased
'variation_purchased', // product variations purchased
'purchased_categories', // product categories
'commerce_coupons', // used coupons
];
foreach ($conditions as $condition) {
$prop = $condition['data_key'];
$operator = $condition['operator'];
if (in_array($prop, $commerceProps)) {
$formattedConditions[] = [
'operator' => $operator,
'value' => $condition['data_value'],
'property' => $prop,
];
} else {
$legacyConditions[] = $condition;
}
}
if ($formattedConditions) {
$hasSubscriber = Subscriber::where('id', $subscriber->id)->where(function ($q) use ($formattedConditions) {
do_action_ref_array('fluentcrm_contacts_filter_fluent_cart', [&$q, $formattedConditions]);
})->first();
if (!$hasSubscriber) {
return false;
}
}
// } else {
// $legacyConditions = $conditions;
// }
if ($legacyConditions) {
$cartCustomer = Customer::query()
->where('email', $subscriber->email)
->when($subscriber->user_id, function ($q) use ($subscriber) {
return $q->orWhere('user_id', $subscriber->user_id);
})
->first();
if (!$cartCustomer) {
return false;
}
}
return $result;
}
}
@@ -0,0 +1,93 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart;
use FluentCrm\App\Models\Campaign;
use FluentCrm\App\Services\Helper as FluentCrmHelper;
use FluentCrm\Framework\Support\Arr;
/**
* Stamps the originating campaign id (`_fc_cid` cookie set by RedirectionHandler when an
* email link is clicked) onto each FluentCart order at creation time, then rolls the
* paid total into the `_campaign_revenue` campaign meta when the order is paid.
*
* This mirrors the WooCommerce/EDD revenue attribution flow used by
* CampaignAnalyticsController so the existing campaign analytics UI works for FluentCart.
*/
class RevenueTracker
{
public function init()
{
add_action('fluent_cart/order_created', [$this, 'attributeOrder'], 10, 1);
add_action('fluent_cart/order_paid', [$this, 'recordRevenue'], 10, 1);
}
/**
* Stamp the originating campaign id on the order while we still have access to
* the visitor's `fc_cid` cookie (the request runs in the user's browser context here;
* by the time `order_paid` fires from a gateway webhook the cookie is gone).
*/
public function attributeOrder($eventData)
{
$cid = $this->getCampaignIdFromCookie();
if (!$cid) {
return;
}
$order = Arr::get($eventData, 'order');
if (!$order || empty($order->id)) {
return;
}
if ($order->getMeta('_fc_cid')) {
return;
}
$order->updateMeta('_fc_cid', $cid);
}
/**
* On payment success, accumulate this order's total into the campaign's
* `_campaign_revenue` meta. Helper::recordCampaignRevenue handles dedup
* (same order_id won't be counted twice) and per-currency bucketing.
*/
public function recordRevenue($eventData)
{
$order = Arr::get($eventData, 'order');
if (!$order || empty($order->id)) {
return;
}
$cid = (int) $order->getMeta('_fc_cid');
if (!$cid) {
return;
}
// FluentCart stores monetary amounts as integer cents — same convention the
// Woo re-sync uses when writing into `_campaign_revenue`.
$amountCents = (int) $order->total_amount;
if ($amountCents <= 0) {
return;
}
$currency = $order->currency ? strtoupper($order->currency) : 'USD';
FluentCrmHelper::recordCampaignRevenue($cid, $amountCents, $order->id, $currency);
}
private function getCampaignIdFromCookie()
{
if (empty($_COOKIE['fc_cid'])) {
return 0;
}
$cid = (int) $_COOKIE['fc_cid'];
if ($cid <= 0) {
return 0;
}
$exists = Campaign::withoutGlobalScopes()->where('id', $cid)->exists();
return $exists ? $cid : 0;
}
}
@@ -0,0 +1,297 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart\SmartCode;
use FluentCart\App\Helpers\Helper;
use FluentCart\App\Models\Order;
use FluentCart\App\Services\Payments\PaymentReceipt;
use FluentCart\App\Services\URL;
use FluentCrm\App\Models\FunnelSubscriber;
use FluentCart\Api\Resource\OrderResource;
class SmartCodeParser {
public static function parseCartOrder($code, $valueKey, $defaultValue, $subscriber)
{
$funnelSub = FunnelSubscriber::where('id', $subscriber->funnel_subscriber_id)->first();
try {
if (!$funnelSub) {
return $defaultValue;
}
$order = OrderResource::view($funnelSub['source_ref_id']);
} catch (\Exception $exception) {
return $defaultValue;
}
if (empty($order) || !isset($order['order'])) {
return $defaultValue;
}
return self::parseOrderProps($order['order'], $valueKey, $defaultValue);
}
public static function parseCartCustomer($code, $valueKey, $defaultValue, $subscriber)
{
// Try to load customer directly from Customer model
$customer = \FluentCart\App\Models\Customer::where('email', $subscriber->email)->first();
if ($customer) {
return self::parseCustomerProps($customer->toArray(), $valueKey, $defaultValue);
}
// Fallback: Load from order payload if direct model not found
try {
$funnelSub = FunnelSubscriber::where('id', $subscriber->funnel_subscriber_id)->first();
if (!$funnelSub) {
return $defaultValue;
}
$order = OrderResource::view($funnelSub['source_ref_id']);
} catch (\Exception $exception) {
return $defaultValue;
}
if (empty($order) || !isset($order['order']['customer'])) {
return $defaultValue;
}
// Ensure array structure
$orderCustomer = is_array($order['order']['customer'])
? $order['order']['customer']
: (method_exists($order['order']['customer'], 'toArray')
? $order['order']['customer']->toArray()
: []);
if (!$orderCustomer) {
return $defaultValue;
}
return self::parseCustomerProps($orderCustomer, $valueKey, $defaultValue);
}
public static function parseCartTransaction($code, $valueKey, $defaultValue, $subscriber)
{
$funnelSub = FunnelSubscriber::where('id', $subscriber->funnel_subscriber_id)->first();
try {
if (!$funnelSub) {
return $defaultValue;
}
$order = OrderResource::view($funnelSub['source_ref_id']);
} catch (\Exception $exception) {
return $defaultValue;
}
return self::parseTransactionProps($order['order']['transactions'], $valueKey, $defaultValue);
}
public static function parseCartReceipt($code, $valueKey, $defaultValue, $subscriber)
{
$funnelSub = FunnelSubscriber::where('id', $subscriber->funnel_subscriber_id)->first();
if (!$funnelSub) {
return $defaultValue;
}
try {
$order = OrderResource::view($funnelSub['source_ref_id']);
} catch (\Exception $exception) {
return $defaultValue;
}
return self::parseReceiptProps($order, $valueKey, $defaultValue);
}
public static function parseOrderProps($order, $valueKey, $defaultValue)
{
if (!$order) {
return $defaultValue;
}
switch ($valueKey) {
case 'order_id':
return $order['id'];
case 'status':
return $order['status'];
case 'invoice_no':
return $order['invoice_no'];
case 'receipt_number':
return $order['receipt_number'];
case 'type':
return $order['type'];
case 'customer_id':
return $order['customer_id'];
case 'payment_method':
return $order['payment_method'];
case 'payment_method_title':
return $order['payment_method_title'];
case 'payment_status':
return $order['payment_status'];
case 'currency':
return $order['currency'];
case 'subtotal':
return Helper::toDecimal($order['subtotal']);
case 'shipping_total':
return Helper::toDecimal($order['shipping_total']);
case 'total_amount':
return Helper::toDecimal($order['total_amount']);
case 'note':
return $order['note'];
case 'completed_at':
return $order['completed_at'];
case 'total_refund':
return Helper::toDecimal($order['total_refund']);
case 'uuid':
return $order['uuid'];
case 'created_at':
return $order['created_at'];
case 'total_paid':
return Helper::toDecimal($order['total_paid']);
case 'shipping_status':
return $order['shipping_status'];
// case 'order_url':
// return URL::getCustomerOrderUrl ($order['uuid']);
default:
return null;
}
}
public static function parseTransactionProps($transaction, $valueKey, $defaultValue)
{
// this method is not called for now check FluentCart->addHooks Method
//Considering the latest transaction
usort($transaction, function ($a, $b) {
return strtotime($b['created_at']) - strtotime($a['created_at']);
});
$transaction = $transaction[0];
if (!$transaction) {
return $defaultValue;
}
switch ($valueKey) {
case 'transaction_id':
return $transaction['id'];
case 'order_id':
return $transaction['order_id'];
case 'order_type':
return $transaction['order_type'];
case 'vendor_charge_id':
return $transaction['vendor_charge_id'];
case 'payment_method':
return $transaction['payment_method'];
case 'payment_mode':
return $transaction['payment_mode'];
case 'payment_method_type':
return $transaction['payment_method_type'];
case 'transaction_type':
return $transaction['transaction_type'];
case 'subscription_id':
return $transaction['subscription_id'];
case 'card_last_4':
return $transaction['card_last_4'];
case 'card_brand':
return $transaction['card_brand'];
case 'status':
return $transaction['status'];
case 'total':
return $transaction['total'];
case 'rate':
return $transaction['rate'];
case 'meta':
return $transaction['meta'];
case 'uuid':
return $transaction['uuid'];
default:
return null;
}
}
public static function parseCustomerProps($customer, $valueKey, $defaultValue)
{
if (!$customer) {
return $defaultValue;
}
switch ($valueKey) {
case 'user_id':
return $customer['user_id'];
case 'contact_id':
return $customer['contact_id'];
case 'first_name':
return $customer['first_name'];
case 'last_name':
return $customer['last_name'];
case 'email':
return $customer['email'];
case 'status':
return $customer['status'];
case 'purchase_value':
return $customer['purchase_value'];
case 'purchase_count':
return $customer['purchase_count'];
case 'first_purchase_date':
return $customer['first_purchase_date'];
case 'last_purchase_date':
return $customer['last_purchase_date'];
case 'aov':
return $customer['aov'];
case 'notes':
return $customer['notes'];
case 'uuid':
return $customer['uuid'];
case 'country':
return $customer['country'];
case 'city':
return $customer['city'];
case 'state':
return $customer['state'];
case 'postcode':
return $customer['postcode'];
default:
return null;
}
}
public static function parseReceiptProps($order, $valueKey, $defaultValue)
{
if (!$order) {
return $defaultValue;
}
switch ($valueKey) {
case 'payment_summary':
return self::PaymentReceipt($order);
case 'order_summary':
return null;
default:
return null;
}
}
private static function paymentReceipt($order)
{
$order = Order::with('order_items')->find($order['order']['id']);
$paymentReceipt = new PaymentReceipt($order);
$showQuantityColumn = false;
// Check if any item in the receipt is not a subscription.
// If such an item exists, set $showQuantityColumn to true.
foreach ($paymentReceipt->getItems() as $item) {
if ($item['payment_type'] !== 'subscription') {
$showQuantityColumn = true;
break;
}
}
$shop = Helper::shopConfig();
$currencySign = $shop['currency_sign'];
ob_start();
do_action('fluent_cart/views/checkout_order_summary', compact('order', 'paymentReceipt', 'showQuantityColumn', 'currencySign'));
return ob_get_clean();
}
}
@@ -0,0 +1,98 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart\SmartCode;
class SmartCodeRegister
{
public static function push()
{
$smartCodesConfig = [
'cart_order' => [
'title' => __('Cart Order', 'fluent-crm'),
'description' => __('Order-related smart codes', 'fluent-crm'),
'shortcodes' => [
'{{cart_order.order_id}}' => __('Order ID', 'fluent-crm'),
'{{cart_order.status}}' => __('Order Status', 'fluent-crm'),
'{{cart_order.invoice_no}}' => __('Invoice Number', 'fluent-crm'),
'{{cart_order.type}}' => __('Order Type', 'fluent-crm'),
'{{cart_order.payment_method}}' => __('Payment Method', 'fluent-crm'),
'{{cart_order.payment_method_title}}' => __('Payment Method Title', 'fluent-crm'),
'{{cart_order.payment_status}}' => __('Payment Status', 'fluent-crm'),
'{{cart_order.currency}}' => __('Currency', 'fluent-crm'),
'{{cart_order.subtotal}}' => __('Order Subtotal', 'fluent-crm'),
'{{cart_order.shipping_total}}' => __('Shipping Total', 'fluent-crm'),
'{{cart_order.total_amount}}' => __('Total Amount', 'fluent-crm'),
'{{cart_order.note}}' => __('Order Note', 'fluent-crm'),
'{{cart_order.completed_at}}' => __('Order Completion Date', 'fluent-crm'),
'{{cart_order.total_refund}}' => __('Total Refund', 'fluent-crm'),
'{{cart_order.created_at}}' => __('Order Creation Date', 'fluent-crm'),
'{{cart_order.total_paid}}' => __('Total Paid Amount', 'fluent-crm'),
'{{cart_order.shipping_status}}' => __('Shipping Status', 'fluent-crm'),
// '##cart_order.order_url##' => 'Order URL',
]
],
'cart_customer' => [
'title' => __('Cart Customer', 'fluent-crm'),
'description' => __('Customer-related smart codes', 'fluent-crm'),
'shortcodes' => [
'{{cart_customer.user_id}}' => __('User ID', 'fluent-crm'),
'{{cart_customer.contact_id}}' => __('Customer Contact ID', 'fluent-crm'),
'{{cart_customer.email}}' => __('Customer Email', 'fluent-crm'),
'{{cart_customer.first_name}}' => __('Customer First Name', 'fluent-crm'),
'{{cart_customer.last_name}}' => __('Customer Last Name', 'fluent-crm'),
'{{cart_customer.status}}' => __('Customer Status', 'fluent-crm'),
'{{cart_customer.purchase_value}}' => __('Total Purchase Value', 'fluent-crm'),
'{{cart_customer.purchase_count}}' => __('Total Purchase Count', 'fluent-crm'),
'{{cart_customer.first_purchase_date}}' => __('First Purchase Date', 'fluent-crm'),
'{{cart_customer.last_purchase_date}}' => __('Last Purchase Date', 'fluent-crm'),
'{{cart_customer.aov}}' => __('Average Order Value (AOV)', 'fluent-crm'),
'{{cart_customer.notes}}' => __('Customer Notes', 'fluent-crm'),
'{{cart_customer.country}}' => __('Customer Country', 'fluent-crm'),
'{{cart_customer.city}}' => __('Customer City', 'fluent-crm'),
'{{cart_customer.state}}' => __('Customer State', 'fluent-crm'),
'{{cart_customer.postcode}}' => __('Customer Postcode', 'fluent-crm'),
]
],
// 'cart_transaction' => [
// 'title' => __('Cart Transaction'),
// 'description' => __('Transaction-related smart codes'),
// 'shortcodes' => [
// '{{cart_transaction.transaction_id}}' => 'Transaction ID',
// '{{cart_transaction.amount}}' => 'Transaction Amount',
// '{{cart_transaction.order_id}}' => 'Order ID',
// '{{cart_transaction.order_type}}' => 'Order Type',
// '{{cart_transaction.payment_method}}' => 'Payment Method',
// '{{cart_transaction.payment_mode}}' => 'Payment Mode',
// '{{cart_transaction.payment_method_type}}' => 'Payment Method Type',
// '{{cart_transaction.transaction_type}}' => 'Transaction Type',
// '{{cart_transaction.subscription_id}}' => 'Subscription ID',
// '{{cart_transaction.status}}' => 'Transaction Status',
// '{{cart_transaction.total}}' => 'Transaction Total',
// '{{cart_transaction.rate}}' => 'Exchange Rate',
// ]
// ],
// 'cart_receipt' => [
// 'title' => __('Cart Receipts'),
// 'description' => __('Transaction-related smart codes'),
// 'shortcodes' => [
// '{{cart_receipt.payment_summary}}' => 'Payment Summary',
// ]
// ],
];
// Dynamically register filters for each smart code group
foreach ($smartCodesConfig as $key => $config) {
add_filter('fluent_crm_funnel_context_smart_codes', function ($codes) use ($key, $config) {
$codes[] = [
'key' => $key,
'title' => $config['title'],
'description' => $config['description'],
'shortcodes' => $config['shortcodes']
];
return $codes;
});
}
}
}
@@ -0,0 +1,218 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers;
use FluentCrm\App\Services\Funnel\FunnelProcessor;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\App\Services\Funnel\BaseTrigger;
use FluentCrm\App\Services\Funnel\FunnelHelper;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\CartHelper;
class OrderCanceledTrigger extends BaseTrigger
{
public function __construct()
{
$this->triggerName = 'fluent_cart/order_status_changed_to_canceled';
$this->priority = 20;
$this->actionArgNum = 1;
parent::__construct();
}
public function getTrigger()
{
return [
'category' => __('FluentCart', 'fluent-crm'),
'label' => __('Order Canceled', 'fluent-crm'),
'description' => __('This will start when an order is canceled', 'fluent-crm'),
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1"><g id="surface1"><path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 2.398438 0 L 21.601562 0 C 22.925781 0 24 1.074219 24 2.398438 L 24 21.601562 C 24 22.925781 22.925781 24 21.601562 24 L 2.398438 24 C 1.074219 24 0 22.925781 0 21.601562 L 0 2.398438 C 0 1.074219 1.074219 0 2.398438 0 Z M 2.398438 0 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 10.925781 16.476562 L 3.769531 16.476562 L 4.894531 13.878906 C 5.222656 13.117188 5.972656 12.625 6.804688 12.625 L 15.328125 12.625 L 14.746094 13.964844 C 14.085938 15.488281 12.585938 16.476562 10.925781 16.476562 Z M 10.925781 16.476562 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 16.851562 11.394531 L 6.789062 11.394531 L 7.367188 10.054688 C 8.027344 8.53125 9.53125 7.542969 11.191406 7.542969 L 19.886719 7.542969 L 18.761719 10.140625 C 18.433594 10.902344 17.683594 11.394531 16.851562 11.394531 Z M 16.851562 11.394531 "/></g></svg>'
];
}
public function getFunnelSettingsDefaults()
{
return [
'subscription_status' => 'subscribed'
];
}
public function getSettingsFields($funnel)
{
return [
'title' => __('Order Canceled', 'fluent-crm'),
'sub_title' => __('This will start when an order is canceled', '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 [
'product_ids' => [],
'product_categories' => [],
// 'purchase_type' => 'all',
'run_multiple' => 'no'
];
}
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 exists in the database','fluent-crm'),
// 'options' => FunnelHelper::getUpdateOptions()
// ],
'product_ids' => [
'type' => 'rest_selector',
'label' => __('Target Products', 'fluent-crm'),
'option_key' => 'fluent_cart_products',
'is_multiple' => true,
'help' => __('Select for which products this automation will run', 'fluent-crm'),
'inline_help' => __('Keep it blank to run to any product purchase', 'fluent-crm')
],
'product_categories' => [
'type' => 'rest_selector',
'label' => __('Or Target Product Categories', 'fluent-crm'),
'option_key' => 'fluent_cart_product_categories',
'is_multiple' => true,
'help' => __('Select for which product category the automation will run', 'fluent-crm'),
'inline_help' => __('Keep it blank to run to any category products', 'fluent-crm')
],
// 'run_only_if_coupon_applied' => [
// 'type' => 'yes_no_check',
// 'label' => '',
// 'check_label' => __('Run automation only if a coupon is applied to the order', '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 enabled, it will restart the automation for a contact if the contact is already in the automation. Otherwise, it will skip if it already exists', 'fluent-crm')
]
];
}
public function handle($funnel, $originalArgs)
{
$orderData = $originalArgs[0] ?? [];
$order = Arr::get($orderData, 'order', []);
$customer = Arr::get($order, 'customer', []);
// $transaction = Arr::get($orderData, 'transaction', []);
$orderId = Arr::get($order, 'id', 0);
// Get the funnel settings and conditions
$settings = Arr::get($funnel, 'settings', []);
$subscriberData = CartHelper::prepareSubscriberData($customer);
if (!is_email($subscriberData['email'])) {
return;
}
$willProcess = $this->isProcessable($funnel, $order, $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'] = (!empty($subscriberData['subscription_status'])) ? $subscriberData['subscription_status'] : 'subscribed';
unset($subscriberData['subscription_status']);
(new FunnelProcessor())->startFunnelSequence($funnel, $subscriberData, [
'source_trigger_name' => $this->triggerName,
'source_ref_id' => $orderId
]);
}
private function isProcessable($funnel, $order, $subscriberData)
{
$conditions = Arr::get($funnel, 'conditions', []);
$isProcessable = $this->checkConditions($conditions, $order, $subscriberData);
if(!$isProcessable){
return false;
}
$subscriber = FunnelHelper::getSubscriber($subscriberData['email']);
// check run_only_one
if ($subscriber) {
$funnelSub = FunnelHelper::ifAlreadyInFunnel($funnel->id, $subscriber->id);
if ($funnelSub) {
$multipleRun = Arr::get($conditions, 'run_multiple') == 'yes';
if ($multipleRun) {
if ($funnelSub->source_ref_id == $order->id) {
return false;
}
FunnelHelper::removeSubscribersFromFunnel($funnel->id, [$subscriber->id]);
}
return $multipleRun;
}
}
return true;
}
public function checkConditions($conditions, $order, $subscriber)
{
$orderItems = Arr::get($order, 'order_items', []);
// Post IDs of ordered products are the product IDs in FluentCart
$orderedProductIds = [];
foreach ($orderItems as $item) {
$productId = $item->post_id;
if ($productId) {
$orderedProductIds[] = $productId;
}
}
$orderProductCategories = CartHelper::getProductCategoriesByIds($orderedProductIds);
$selectedProductIds = Arr::get($conditions, 'product_ids', []);
$selectedProductCategories = Arr::get($conditions, 'product_categories', []);
// If no products or categories are selected, return true
if (empty($selectedProductIds) && empty($selectedProductCategories)) {
return true;
}
// Check for matches in product IDs and categories
$productMatch = !empty($selectedProductIds) && !empty(array_intersect($selectedProductIds, $orderedProductIds));
$categoryMatch = !empty($selectedProductCategories) && !empty(array_intersect($selectedProductCategories, $orderProductCategories));
// Return true if either matches
return $productMatch || $categoryMatch;
}
}
@@ -0,0 +1,218 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers;
use FluentCrm\App\Services\Funnel\FunnelProcessor;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\App\Services\Funnel\BaseTrigger;
use FluentCrm\App\Services\Funnel\FunnelHelper;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\CartHelper;
class OrderDeliveredTrigger extends BaseTrigger
{
public function __construct()
{
$this->triggerName = 'fluent_cart/shipping_status_changed_to_delivered';
$this->priority = 20;
$this->actionArgNum = 1;
parent::__construct();
}
public function getTrigger()
{
return [
'category' => __('FluentCart', 'fluent-crm'),
'label' => __('Order Delivered', 'fluent-crm'),
'description' => __('This will start when a successful order is delivered', 'fluent-crm'),
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1"><g id="surface1"><path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 2.398438 0 L 21.601562 0 C 22.925781 0 24 1.074219 24 2.398438 L 24 21.601562 C 24 22.925781 22.925781 24 21.601562 24 L 2.398438 24 C 1.074219 24 0 22.925781 0 21.601562 L 0 2.398438 C 0 1.074219 1.074219 0 2.398438 0 Z M 2.398438 0 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 10.925781 16.476562 L 3.769531 16.476562 L 4.894531 13.878906 C 5.222656 13.117188 5.972656 12.625 6.804688 12.625 L 15.328125 12.625 L 14.746094 13.964844 C 14.085938 15.488281 12.585938 16.476562 10.925781 16.476562 Z M 10.925781 16.476562 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 16.851562 11.394531 L 6.789062 11.394531 L 7.367188 10.054688 C 8.027344 8.53125 9.53125 7.542969 11.191406 7.542969 L 19.886719 7.542969 L 18.761719 10.140625 C 18.433594 10.902344 17.683594 11.394531 16.851562 11.394531 Z M 16.851562 11.394531 "/></g></svg>'
];
}
public function getFunnelSettingsDefaults()
{
return [
'subscription_status' => 'subscribed'
];
}
public function getSettingsFields($funnel)
{
return [
'title' => __('Order Delivered', 'fluent-crm'),
'sub_title' => __('This will start when an order is delivered', '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 [
'product_ids' => [],
'product_categories' => [],
// 'purchase_type' => 'all',
'run_multiple' => 'no'
];
}
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 exists in the database','fluent-crm'),
// 'options' => FunnelHelper::getUpdateOptions()
// ],
'product_ids' => [
'type' => 'rest_selector',
'label' => __('Target Products', 'fluent-crm'),
'option_key' => 'fluent_cart_products',
'is_multiple' => true,
'help' => __('Select for which products this automation will run', 'fluent-crm'),
'inline_help' => __('Keep it blank to run to any product purchase', 'fluent-crm')
],
'product_categories' => [
'type' => 'rest_selector',
'label' => __('Or Target Product Categories', 'fluent-crm'),
'option_key' => 'fluent_cart_product_categories',
'is_multiple' => true,
'help' => __('Select for which product category the automation will run', 'fluent-crm'),
'inline_help' => __('Keep it blank to run to any category products', 'fluent-crm')
],
// 'run_only_if_coupon_applied' => [
// 'type' => 'yes_no_check',
// 'label' => '',
// 'check_label' => __('Run automation only if a coupon is applied to the order', '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 enabled, it will restart the automation for a contact if the contact is already in the automation. Otherwise, it will skip if it already exists', 'fluent-crm')
]
];
}
public function handle($funnel, $originalArgs)
{
$orderData = $originalArgs[0] ?? [];
$order = Arr::get($orderData, 'order', []);
$customer = Arr::get($order, 'customer', []);
// $transaction = Arr::get($orderData, 'transaction', []);
$orderId = Arr::get($order, 'id', 0);
// Get the funnel settings and conditions
$settings = Arr::get($funnel, 'settings', []);
$subscriberData = CartHelper::prepareSubscriberData($customer);
if (!is_email($subscriberData['email'])) {
return;
}
$willProcess = $this->isProcessable($funnel, $order, $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'] = (!empty($subscriberData['subscription_status'])) ? $subscriberData['subscription_status'] : 'subscribed';
unset($subscriberData['subscription_status']);
(new FunnelProcessor())->startFunnelSequence($funnel, $subscriberData, [
'source_trigger_name' => $this->triggerName,
'source_ref_id' => $orderId
]);
}
private function isProcessable($funnel, $order, $subscriberData)
{
$conditions = Arr::get($funnel, 'conditions', []);
$isProcessable = $this->checkConditions($conditions, $order, $subscriberData);
if(!$isProcessable){
return false;
}
$subscriber = FunnelHelper::getSubscriber($subscriberData['email']);
// check run_only_one
if ($subscriber) {
$funnelSub = FunnelHelper::ifAlreadyInFunnel($funnel->id, $subscriber->id);
if ($funnelSub) {
$multipleRun = Arr::get($conditions, 'run_multiple') == 'yes';
if ($multipleRun) {
if ($funnelSub->source_ref_id == $order->id) {
return false;
}
FunnelHelper::removeSubscribersFromFunnel($funnel->id, [$subscriber->id]);
}
return $multipleRun;
}
}
return true;
}
public function checkConditions($conditions, $order, $subscriber)
{
$orderItems = Arr::get($order, 'order_items', []);
// Post IDs of ordered products are the product IDs in FluentCart
$orderedProductIds = [];
foreach ($orderItems as $item) {
$productId = $item->post_id;
if ($productId) {
$orderedProductIds[] = $productId;
}
}
$orderProductCategories = CartHelper::getProductCategoriesByIds($orderedProductIds);
$selectedProductIds = Arr::get($conditions, 'product_ids', []);
$selectedProductCategories = Arr::get($conditions, 'product_categories', []);
// If no products or categories are selected, return true
if (empty($selectedProductIds) && empty($selectedProductCategories)) {
return true;
}
// Check for matches in product IDs and categories
$productMatch = !empty($selectedProductIds) && !empty(array_intersect($selectedProductIds, $orderedProductIds));
$categoryMatch = !empty($selectedProductCategories) && !empty(array_intersect($selectedProductCategories, $orderProductCategories));
// Return true if either matches
return $productMatch || $categoryMatch;
}
}
@@ -0,0 +1,218 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers;
use FluentCrm\App\Services\Funnel\FunnelProcessor;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\App\Services\Funnel\BaseTrigger;
use FluentCrm\App\Services\Funnel\FunnelHelper;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\CartHelper;
class OrderPaidTrigger extends BaseTrigger
{
public function __construct()
{
$this->triggerName = 'fluent_cart/order_paid_done';
$this->priority = 20;
$this->actionArgNum = 1;
parent::__construct();
}
public function getTrigger()
{
return [
'category' => __('FluentCart', 'fluent-crm'),
'label' => __('Order Paid (Payment/Subscription)', 'fluent-crm'),
'description' => __('This will start when a successful order is created', 'fluent-crm'),
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1"><g id="surface1"><path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 2.398438 0 L 21.601562 0 C 22.925781 0 24 1.074219 24 2.398438 L 24 21.601562 C 24 22.925781 22.925781 24 21.601562 24 L 2.398438 24 C 1.074219 24 0 22.925781 0 21.601562 L 0 2.398438 C 0 1.074219 1.074219 0 2.398438 0 Z M 2.398438 0 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 10.925781 16.476562 L 3.769531 16.476562 L 4.894531 13.878906 C 5.222656 13.117188 5.972656 12.625 6.804688 12.625 L 15.328125 12.625 L 14.746094 13.964844 C 14.085938 15.488281 12.585938 16.476562 10.925781 16.476562 Z M 10.925781 16.476562 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 16.851562 11.394531 L 6.789062 11.394531 L 7.367188 10.054688 C 8.027344 8.53125 9.53125 7.542969 11.191406 7.542969 L 19.886719 7.542969 L 18.761719 10.140625 C 18.433594 10.902344 17.683594 11.394531 16.851562 11.394531 Z M 16.851562 11.394531 "/></g></svg>'
];
}
public function getFunnelSettingsDefaults()
{
return [
'subscription_status' => 'subscribed'
];
}
public function getSettingsFields($funnel)
{
return [
'title' => __('Order Paid (Payment/Subscription)', 'fluent-crm'),
'sub_title' => __('This will start when an order is paid', '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 [
'product_ids' => [],
'product_categories' => [],
// 'purchase_type' => 'all',
'run_multiple' => 'no'
];
}
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 exists in the database','fluent-crm'),
// 'options' => FunnelHelper::getUpdateOptions()
// ],
'product_ids' => [
'type' => 'rest_selector',
'label' => __('Target Products', 'fluent-crm'),
'option_key' => 'fluent_cart_products',
'is_multiple' => true,
'help' => __('Select for which products this automation will run', 'fluent-crm'),
'inline_help' => __('Keep it blank to run to any product purchase', 'fluent-crm')
],
'product_categories' => [
'type' => 'rest_selector',
'label' => __('Or Target Product Categories', 'fluent-crm'),
'option_key' => 'fluent_cart_product_categories',
'is_multiple' => true,
'help' => __('Select for which product category the automation will run', 'fluent-crm'),
'inline_help' => __('Keep it blank to run to any category products', 'fluent-crm')
],
// 'run_only_if_coupon_applied' => [
// 'type' => 'yes_no_check',
// 'label' => '',
// 'check_label' => __('Run automation only if a coupon is applied to the order', '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 enabled, it will restart the automation for a contact if the contact is already in the automation. Otherwise, it will skip if it already exists', 'fluent-crm')
]
];
}
public function handle($funnel, $originalArgs)
{
$orderData = $originalArgs[0] ?? [];
$order = Arr::get($orderData, 'order', []);
$customer = Arr::get($orderData, 'customer', []);
$transaction = Arr::get($orderData, 'transaction', []);
$orderId = Arr::get($order, 'id', 0);
// Get the funnel settings and conditions
$settings = Arr::get($funnel, 'settings', []);
$subscriberData = CartHelper::prepareSubscriberData($customer);
if (!is_email($subscriberData['email'])) {
return;
}
$willProcess = $this->isProcessable($funnel, $order, $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'] = (!empty($subscriberData['subscription_status'])) ? $subscriberData['subscription_status'] : 'subscribed';
unset($subscriberData['subscription_status']);
(new FunnelProcessor())->startFunnelSequence($funnel, $subscriberData, [
'source_trigger_name' => $this->triggerName,
'source_ref_id' => $orderId
]);
}
private function isProcessable($funnel, $order, $subscriberData)
{
$conditions = Arr::get($funnel, 'conditions', []);
$isProcessable = $this->checkConditions($conditions, $order, $subscriberData);
if(!$isProcessable){
return false;
}
$subscriber = FunnelHelper::getSubscriber($subscriberData['email']);
// check run_only_one
if ($subscriber) {
$funnelSub = FunnelHelper::ifAlreadyInFunnel($funnel->id, $subscriber->id);
if ($funnelSub) {
$multipleRun = Arr::get($conditions, 'run_multiple') == 'yes';
if ($multipleRun) {
if ($funnelSub->source_ref_id == $order->id) {
return false;
}
FunnelHelper::removeSubscribersFromFunnel($funnel->id, [$subscriber->id]);
}
return $multipleRun;
}
}
return true;
}
public function checkConditions($conditions, $order, $subscriber)
{
$orderItems = Arr::get($order, 'order_items', []);
// Post IDs of ordered products are the product IDs in FluentCart
$orderedProductIds = [];
foreach ($orderItems as $item) {
$productId = $item->post_id;
if ($productId) {
$orderedProductIds[] = $productId;
}
}
$orderProductCategories = CartHelper::getProductCategoriesByIds($orderedProductIds);
$selectedProductIds = Arr::get($conditions, 'product_ids', []);
$selectedProductCategories = Arr::get($conditions, 'product_categories', []);
// If no products or categories are selected, return true
if (empty($selectedProductIds) && empty($selectedProductCategories)) {
return true;
}
// Check for matches in product IDs and categories
$productMatch = !empty($selectedProductIds) && !empty(array_intersect($selectedProductIds, $orderedProductIds));
$categoryMatch = !empty($selectedProductCategories) && !empty(array_intersect($selectedProductCategories, $orderProductCategories));
// Return true if either matches
return $productMatch || $categoryMatch;
}
}
@@ -0,0 +1,218 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers;
use FluentCrm\App\Services\Funnel\FunnelProcessor;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\App\Services\Funnel\BaseTrigger;
use FluentCrm\App\Services\Funnel\FunnelHelper;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\CartHelper;
class OrderRefundedTrigger extends BaseTrigger
{
public function __construct()
{
$this->triggerName = 'fluent_cart/order_fully_refunded';
$this->priority = 20;
$this->actionArgNum = 1;
parent::__construct();
}
public function getTrigger()
{
return [
'category' => __('FluentCart', 'fluent-crm'),
'label' => __('Order Refunded (Full)', 'fluent-crm'),
'description' => __('This will start when a successful order is refunded', 'fluent-crm'),
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1"><g id="surface1"><path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 2.398438 0 L 21.601562 0 C 22.925781 0 24 1.074219 24 2.398438 L 24 21.601562 C 24 22.925781 22.925781 24 21.601562 24 L 2.398438 24 C 1.074219 24 0 22.925781 0 21.601562 L 0 2.398438 C 0 1.074219 1.074219 0 2.398438 0 Z M 2.398438 0 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 10.925781 16.476562 L 3.769531 16.476562 L 4.894531 13.878906 C 5.222656 13.117188 5.972656 12.625 6.804688 12.625 L 15.328125 12.625 L 14.746094 13.964844 C 14.085938 15.488281 12.585938 16.476562 10.925781 16.476562 Z M 10.925781 16.476562 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 16.851562 11.394531 L 6.789062 11.394531 L 7.367188 10.054688 C 8.027344 8.53125 9.53125 7.542969 11.191406 7.542969 L 19.886719 7.542969 L 18.761719 10.140625 C 18.433594 10.902344 17.683594 11.394531 16.851562 11.394531 Z M 16.851562 11.394531 "/></g></svg>'
];
}
public function getFunnelSettingsDefaults()
{
return [
'subscription_status' => 'subscribed'
];
}
public function getSettingsFields($funnel)
{
return [
'title' => __('Order Refunded (Full)', 'fluent-crm'),
'sub_title' => __('This will start when an order is refunded', '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 [
'product_ids' => [],
'product_categories' => [],
// 'purchase_type' => 'all',
'run_multiple' => 'no'
];
}
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 exists in the database','fluent-crm'),
// 'options' => FunnelHelper::getUpdateOptions()
// ],
'product_ids' => [
'type' => 'rest_selector',
'label' => __('Target Products', 'fluent-crm'),
'option_key' => 'fluent_cart_products',
'is_multiple' => true,
'help' => __('Select for which products this automation will run', 'fluent-crm'),
'inline_help' => __('Keep it blank to run to any product purchase', 'fluent-crm')
],
'product_categories' => [
'type' => 'rest_selector',
'label' => __('Or Target Product Categories', 'fluent-crm'),
'option_key' => 'fluent_cart_product_categories',
'is_multiple' => true,
'help' => __('Select for which product category the automation will run', 'fluent-crm'),
'inline_help' => __('Keep it blank to run to any category products', 'fluent-crm')
],
// 'run_only_if_coupon_applied' => [
// 'type' => 'yes_no_check',
// 'label' => '',
// 'check_label' => __('Run automation only if a coupon is applied to the order', '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 enabled, it will restart the automation for a contact if the contact is already in the automation. Otherwise, it will skip if it already exists', 'fluent-crm')
]
];
}
public function handle($funnel, $originalArgs)
{
$orderData = $originalArgs[0] ?? [];
$order = Arr::get($orderData, 'order', []);
$customer = Arr::get($order, 'customer', []);
// $transaction = Arr::get($orderData, 'transaction', []);
$orderId = Arr::get($order, 'id', 0);
// Get the funnel settings and conditions
$settings = Arr::get($funnel, 'settings', []);
$subscriberData = CartHelper::prepareSubscriberData($customer);
if (!is_email($subscriberData['email'])) {
return;
}
$willProcess = $this->isProcessable($funnel, $order, $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'] = (!empty($subscriberData['subscription_status'])) ? $subscriberData['subscription_status'] : 'subscribed';
unset($subscriberData['subscription_status']);
(new FunnelProcessor())->startFunnelSequence($funnel, $subscriberData, [
'source_trigger_name' => $this->triggerName,
'source_ref_id' => $orderId
]);
}
private function isProcessable($funnel, $order, $subscriberData)
{
$conditions = Arr::get($funnel, 'conditions', []);
$isProcessable = $this->checkConditions($conditions, $order, $subscriberData);
if(!$isProcessable){
return false;
}
$subscriber = FunnelHelper::getSubscriber($subscriberData['email']);
// check run_only_one
if ($subscriber) {
$funnelSub = FunnelHelper::ifAlreadyInFunnel($funnel->id, $subscriber->id);
if ($funnelSub) {
$multipleRun = Arr::get($conditions, 'run_multiple') == 'yes';
if ($multipleRun) {
if ($funnelSub->source_ref_id == $order->id) {
return false;
}
FunnelHelper::removeSubscribersFromFunnel($funnel->id, [$subscriber->id]);
}
return $multipleRun;
}
}
return true;
}
public function checkConditions($conditions, $order, $subscriber)
{
$orderItems = Arr::get($order, 'order_items', []);
// Post IDs of ordered products are the product IDs in FluentCart
$orderedProductIds = [];
foreach ($orderItems as $item) {
$productId = $item->post_id;
if ($productId) {
$orderedProductIds[] = $productId;
}
}
$orderProductCategories = CartHelper::getProductCategoriesByIds($orderedProductIds);
$selectedProductIds = Arr::get($conditions, 'product_ids', []);
$selectedProductCategories = Arr::get($conditions, 'product_categories', []);
// If no products or categories are selected, return true
if (empty($selectedProductIds) && empty($selectedProductCategories)) {
return true;
}
// Check for matches in product IDs and categories
$productMatch = !empty($selectedProductIds) && !empty(array_intersect($selectedProductIds, $orderedProductIds));
$categoryMatch = !empty($selectedProductCategories) && !empty(array_intersect($selectedProductCategories, $orderProductCategories));
// Return true if either matches
return $productMatch || $categoryMatch;
}
}
@@ -0,0 +1,218 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers;
use FluentCrm\App\Services\Funnel\FunnelProcessor;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\App\Services\Funnel\BaseTrigger;
use FluentCrm\App\Services\Funnel\FunnelHelper;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\CartHelper;
class OrderShippedTrigger extends BaseTrigger
{
public function __construct()
{
$this->triggerName = 'fluent_cart/shipping_status_changed_to_shipped';
$this->priority = 20;
$this->actionArgNum = 1;
parent::__construct();
}
public function getTrigger()
{
return [
'category' => __('FluentCart', 'fluent-crm'),
'label' => __('Order Shipped', 'fluent-crm'),
'description' => __('This will start when a successful order is shipped', 'fluent-crm'),
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1"><g id="surface1"><path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 2.398438 0 L 21.601562 0 C 22.925781 0 24 1.074219 24 2.398438 L 24 21.601562 C 24 22.925781 22.925781 24 21.601562 24 L 2.398438 24 C 1.074219 24 0 22.925781 0 21.601562 L 0 2.398438 C 0 1.074219 1.074219 0 2.398438 0 Z M 2.398438 0 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 10.925781 16.476562 L 3.769531 16.476562 L 4.894531 13.878906 C 5.222656 13.117188 5.972656 12.625 6.804688 12.625 L 15.328125 12.625 L 14.746094 13.964844 C 14.085938 15.488281 12.585938 16.476562 10.925781 16.476562 Z M 10.925781 16.476562 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 16.851562 11.394531 L 6.789062 11.394531 L 7.367188 10.054688 C 8.027344 8.53125 9.53125 7.542969 11.191406 7.542969 L 19.886719 7.542969 L 18.761719 10.140625 C 18.433594 10.902344 17.683594 11.394531 16.851562 11.394531 Z M 16.851562 11.394531 "/></g></svg>'
];
}
public function getFunnelSettingsDefaults()
{
return [
'subscription_status' => 'subscribed'
];
}
public function getSettingsFields($funnel)
{
return [
'title' => __('Order Shipped', 'fluent-crm'),
'sub_title' => __('This will start when an order is shipped', '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 [
'product_ids' => [],
'product_categories' => [],
// 'purchase_type' => 'all',
'run_multiple' => 'no'
];
}
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 exists in the database','fluent-crm'),
// 'options' => FunnelHelper::getUpdateOptions()
// ],
'product_ids' => [
'type' => 'rest_selector',
'label' => __('Target Products', 'fluent-crm'),
'option_key' => 'fluent_cart_products',
'is_multiple' => true,
'help' => __('Select for which products this automation will run', 'fluent-crm'),
'inline_help' => __('Keep it blank to run to any product purchase', 'fluent-crm')
],
'product_categories' => [
'type' => 'rest_selector',
'label' => __('Or Target Product Categories', 'fluent-crm'),
'option_key' => 'fluent_cart_product_categories',
'is_multiple' => true,
'help' => __('Select for which product category the automation will run', 'fluent-crm'),
'inline_help' => __('Keep it blank to run to any category products', 'fluent-crm')
],
// 'run_only_if_coupon_applied' => [
// 'type' => 'yes_no_check',
// 'label' => '',
// 'check_label' => __('Run automation only if a coupon is applied to the order', '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 enabled, it will restart the automation for a contact if the contact is already in the automation. Otherwise, it will skip if it already exists', 'fluent-crm')
]
];
}
public function handle($funnel, $originalArgs)
{
$orderData = $originalArgs[0] ?? [];
$order = Arr::get($orderData, 'order', []);
$customer = Arr::get($order, 'customer', []);
// $transaction = Arr::get($orderData, 'transaction', []);
$orderId = Arr::get($order, 'id', 0);
// Get the funnel settings and conditions
$settings = Arr::get($funnel, 'settings', []);
$subscriberData = CartHelper::prepareSubscriberData($customer);
if (!is_email($subscriberData['email'])) {
return;
}
$willProcess = $this->isProcessable($funnel, $order, $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'] = (!empty($subscriberData['subscription_status'])) ? $subscriberData['subscription_status'] : 'subscribed';
unset($subscriberData['subscription_status']);
(new FunnelProcessor())->startFunnelSequence($funnel, $subscriberData, [
'source_trigger_name' => $this->triggerName,
'source_ref_id' => $orderId
]);
}
private function isProcessable($funnel, $order, $subscriberData)
{
$conditions = Arr::get($funnel, 'conditions', []);
$isProcessable = $this->checkConditions($conditions, $order, $subscriberData);
if(!$isProcessable){
return false;
}
$subscriber = FunnelHelper::getSubscriber($subscriberData['email']);
// check run_only_one
if ($subscriber) {
$funnelSub = FunnelHelper::ifAlreadyInFunnel($funnel->id, $subscriber->id);
if ($funnelSub) {
$multipleRun = Arr::get($conditions, 'run_multiple') == 'yes';
if ($multipleRun) {
if ($funnelSub->source_ref_id == $order->id) {
return false;
}
FunnelHelper::removeSubscribersFromFunnel($funnel->id, [$subscriber->id]);
}
return $multipleRun;
}
}
return true;
}
public function checkConditions($conditions, $order, $subscriber)
{
$orderItems = Arr::get($order, 'order_items', []);
// Post IDs of ordered products are the product IDs in FluentCart
$orderedProductIds = [];
foreach ($orderItems as $item) {
$productId = $item->post_id;
if ($productId) {
$orderedProductIds[] = $productId;
}
}
$orderProductCategories = CartHelper::getProductCategoriesByIds($orderedProductIds);
$selectedProductIds = Arr::get($conditions, 'product_ids', []);
$selectedProductCategories = Arr::get($conditions, 'product_categories', []);
// If no products or categories are selected, return true
if (empty($selectedProductIds) && empty($selectedProductCategories)) {
return true;
}
// Check for matches in product IDs and categories
$productMatch = !empty($selectedProductIds) && !empty(array_intersect($selectedProductIds, $orderedProductIds));
$categoryMatch = !empty($selectedProductCategories) && !empty(array_intersect($selectedProductCategories, $orderProductCategories));
// Return true if either matches
return $productMatch || $categoryMatch;
}
}
@@ -0,0 +1,223 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers;
use FluentCart\App\Helpers\Status;
use FluentCrm\App\Services\Funnel\BaseTrigger;
use FluentCrm\App\Services\Funnel\FunnelHelper;
use FluentCrm\App\Services\Funnel\FunnelProcessor;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\CartHelper;
class OrderStatusChangedTrigger extends BaseTrigger
{
public function __construct()
{
$this->triggerName = 'fluent_cart/order_status_changed';
$this->priority = 20;
$this->actionArgNum = 1;
parent::__construct();
}
public function getTrigger()
{
return [
'category' => __('FluentCart', 'fluent-crm'),
'label' => __('Order Status Changed', 'fluent-crm'),
'description' => __('This funnel will start when an order status updates', 'fluent-crm'),
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1"><g id="surface1"><path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 2.398438 0 L 21.601562 0 C 22.925781 0 24 1.074219 24 2.398438 L 24 21.601562 C 24 22.925781 22.925781 24 21.601562 24 L 2.398438 24 C 1.074219 24 0 22.925781 0 21.601562 L 0 2.398438 C 0 1.074219 1.074219 0 2.398438 0 Z M 2.398438 0 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 10.925781 16.476562 L 3.769531 16.476562 L 4.894531 13.878906 C 5.222656 13.117188 5.972656 12.625 6.804688 12.625 L 15.328125 12.625 L 14.746094 13.964844 C 14.085938 15.488281 12.585938 16.476562 10.925781 16.476562 Z M 10.925781 16.476562 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 16.851562 11.394531 L 6.789062 11.394531 L 7.367188 10.054688 C 8.027344 8.53125 9.53125 7.542969 11.191406 7.542969 L 19.886719 7.542969 L 18.761719 10.140625 C 18.433594 10.902344 17.683594 11.394531 16.851562 11.394531 Z M 16.851562 11.394531 "/></g></svg>'
];
}
public function getFunnelSettingsDefaults()
{
return [
'subscription_status' => 'subscribed'
];
}
public function getSettingsFields($funnel)
{
return [
'title' => __('FluentCart Order Status Changed', 'fluent-crm'),
'sub_title' => __('This Funnel will start when a Order status will change from one state to another', '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 [
'product_ids' => [],
'product_categories' => [],
'from_status' => 'any',
'to_status' => 'any',
'run_multiple' => 'no'
];
}
public function getConditionFields($funnel)
{
$orderStatuses = Status::getOrderStatuses();
$formattedStatuses = [[
'id' => 'any',
'title' => __('Any', 'fluent-crm')
]];
foreach ($orderStatuses as $statusId => $statusName) {
$formattedStatuses[] = [
'id' => $statusId,
'title' => $statusName
];
}
return [
'product_ids' => [
'type' => 'rest_selector',
'option_key' => 'fluent_cart_products',
'is_multiple' => true,
'label' => __('Target Products', 'fluent-crm'),
'help' => __('Select for which products this automation will run', 'fluent-crm'),
'inline_help' => __('Keep it blank to run for any product\'s order status change', 'fluent-crm'),
],
'product_categories' => [
'type' => 'rest_selector',
'option_key' => 'fluent_cart_product_categories',
'is_multiple' => true,
'label' => __('Or Target Product Categories', 'fluent-crm'),
'help' => __('Select for which product category the automation will run', 'fluent-crm'),
'inline_help' => __('Keep it blank to run to any category products', 'fluent-crm'),
],
'from_status' => [
'type' => 'select',
'label' => __('From Order Status', 'fluent-crm'),
'help' => __('The current status that will trigger an action when it changes from this status to the \'To Order Status.\'', 'fluent-crm'),
'options' => $formattedStatuses
],
'to_status' => [
'type' => 'select',
'label' => __('To Order Status', 'fluent-crm'),
'help' => __('The target status that will trigger an action when the order moves from the \'From Order Status\' to this status.', 'fluent-crm'),
'options' => $formattedStatuses
],
'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 enabled, it will restart the automation for a contact if the contact is already in the automation. Otherwise, it will skip if it already exists', 'fluent-crm')
]
];
}
public function handle($funnel, $originalArgs)
{
$orderData = $originalArgs[0] ?? [];
$order = Arr::get($orderData, 'order', []);
$fromStatus = Arr::get($orderData, 'old_status', '');
$toStatus = Arr::get($orderData, 'new_status', '');
$customer = Arr::get($order, 'customer');
$orderId = Arr::get($order, 'id', 0);
$subscriberData = CartHelper::prepareSubscriberData($customer);
if (!is_email($subscriberData['email'])) {
return;
}
$willProcess = $this->isProcessable($funnel, $subscriberData, $fromStatus, $toStatus, $order);
$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'] = (!empty($subscriberData['subscription_status'])) ? $subscriberData['subscription_status'] : 'subscribed';
unset($subscriberData['subscription_status']);
(new FunnelProcessor())->startFunnelSequence($funnel, $subscriberData, [
'source_trigger_name' => $this->triggerName,
'source_ref_id' => $orderId
]);
}
private function isProcessable($funnel, $subscriberData, $fromStatus, $toStatus, $order)
{
$conditions = (array)$funnel->conditions;
$orderItems = Arr::get($order, 'order_items', []);
// Post IDs of ordered products are the product IDs in FluentCart
$orderedProductIds = [];
foreach ($orderItems as $item) {
$productId = $item->post_id;
if ($productId) {
$orderedProductIds[] = $productId;
}
}
$orderProductCategories = CartHelper::getProductCategoriesByIds($orderedProductIds);
$selectedProductIds = Arr::get($conditions, 'product_ids', []);
$selectedProductCategories = Arr::get($conditions, 'product_categories', []);
if (!empty($selectedProductIds) || !empty($selectedProductCategories)) {
$productMatch = !empty($selectedProductIds) && !empty(array_intersect($selectedProductIds, $orderedProductIds));
$categoryMatch = !empty($selectedProductCategories) && !empty(array_intersect($selectedProductCategories, $orderProductCategories));
if (!$productMatch && !$categoryMatch) {
return false;
}
}
$fromCondition = Arr::get($conditions, 'from_status', 'any');
if ($fromCondition !== 'any' && $fromCondition !== $fromStatus) {
return false;
}
$toCondition = Arr::get($conditions, 'to_status', 'any');
if ($toCondition !== 'any' && $toCondition !== $toStatus) {
return false;
}
$subscriber = FunnelHelper::getSubscriber($subscriberData['email']);
if ($subscriber) {
$funnelSub = FunnelHelper::ifAlreadyInFunnel($funnel->id, $subscriber->id);
if ($funnelSub) {
$multipleRun = Arr::get($conditions, 'run_multiple') == 'yes';
if ($multipleRun) {
FunnelHelper::removeSubscribersFromFunnel($funnel->id, [$subscriber->id]);
}
return $multipleRun;
}
}
return true;
}
}
@@ -0,0 +1,183 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers;
use FluentCrm\App\Services\Funnel\BaseTrigger;
use FluentCrm\App\Services\Funnel\FunnelHelper;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\CartHelper;
class SubscriptionActivatedTrigger extends BaseTrigger
{
public function __construct()
{
$this->triggerName = 'fluent_cart/subscription_activated';
$this->priority = 20;
$this->actionArgNum = 1;
parent::__construct();
}
public function getTrigger()
{
return [
'ribbon' => 'subscription',
'category' => __('FluentCart', 'fluent-crm'),
'label' => __('Subscription Activated', 'fluent-crm'),
'description' => __('This will start when a subscription is activated', 'fluent-crm'),
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1"><g id="surface1"><path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 2.398438 0 L 21.601562 0 C 22.925781 0 24 1.074219 24 2.398438 L 24 21.601562 C 24 22.925781 22.925781 24 21.601562 24 L 2.398438 24 C 1.074219 24 0 22.925781 0 21.601562 L 0 2.398438 C 0 1.074219 1.074219 0 2.398438 0 Z M 2.398438 0 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 10.925781 16.476562 L 3.769531 16.476562 L 4.894531 13.878906 C 5.222656 13.117188 5.972656 12.625 6.804688 12.625 L 15.328125 12.625 L 14.746094 13.964844 C 14.085938 15.488281 12.585938 16.476562 10.925781 16.476562 Z M 10.925781 16.476562 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 16.851562 11.394531 L 6.789062 11.394531 L 7.367188 10.054688 C 8.027344 8.53125 9.53125 7.542969 11.191406 7.542969 L 19.886719 7.542969 L 18.761719 10.140625 C 18.433594 10.902344 17.683594 11.394531 16.851562 11.394531 Z M 16.851562 11.394531 "/></g></svg>'
];
}
public function getFunnelSettingsDefaults()
{
return [
'subscription_status' => 'subscribed',
];
}
public function getSettingsFields($funnel)
{
$statuses = fluentcrm_subscriber_editable_statuses(true);
return [
'title' => __('Subscription Activated', 'fluent-crm'),
'sub_title' => __('This will start when a subscription is activated', 'fluent-crm'),
'fields' => [
'subscription_status' => [
'type' => 'select',
'options' => $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 getConditionFields($funnel)
{
return [
'product_ids' => [
'type' => 'rest_selector',
'label' => __('Target Products (Subscription Only)', 'fluent-crm'),
'option_key' => 'fluent_cart_subscription_products',
'is_multiple' => true,
'help' => __('Select the products you want to include in the automation.', 'fluent-crm'),
'inline_help' => __('You can select multiple products. If you want to run for all products, then leave it empty', '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 enabled, it will restart the automation for a contact if the contact is already in the automation. Otherwise, it will skip if it already exists', 'fluent-crm'),
],
];
}
public function getFunnelConditionDefaults($funnel)
{
return [
'product_ids' => [],
'run_multiple' => 'no'
];
}
public function handle($funnel, $originalArgs)
{
$subscriptionData = $originalArgs[0];
$subscription = $subscriptionData['subscription'];
$order = $subscriptionData['order'];
$customer = $subscriptionData['customer'];
$subscriberData = CartHelper::prepareSubscriberData($customer);
if (!is_email($subscriberData['email'])) {
return;
}
$willProcess = $this->isProcessable($funnel, $order, $subscriberData);
if (!$willProcess) {
return;
}
$subscriberData = wp_parse_args($subscriberData, $funnel->settings);
$subscriberData['status'] = $subscriberData['subscription_status'];
unset($subscriberData['subscription_status']);
(new \FluentCrm\App\Services\Funnel\FunnelProcessor())->startFunnelSequence($funnel, $subscriberData, [
'source_trigger_name' => $this->triggerName,
'source_ref_id' => $order->id, // optional
]);
}
public function isProcessable($funnel, $order, $subscriberData)
{
$conditions = Arr::get($funnel, 'conditions', []);
$isProcessable = $this->checkConditions($conditions, $order, $subscriberData);
if(!$isProcessable){
return false;
}
$subscriber = FunnelHelper::getSubscriber($subscriberData['email']);
// check run_only_one
if ($subscriber) {
$funnelSub = FunnelHelper::ifAlreadyInFunnel($funnel->id, $subscriber->id);
if ($funnelSub) {
$multipleRun = Arr::get($conditions, 'run_multiple') == 'yes';
if ($multipleRun) {
if ($funnelSub->source_ref_id == $order->id) {
return false;
}
FunnelHelper::removeSubscribersFromFunnel($funnel->id, [$subscriber->id]);
}
return $multipleRun;
}
}
return true;
}
private function checkConditions($conditions, $order, $subscriber)
{
$selectedProductIds = Arr::get($conditions, 'product_ids', []);
if (empty($selectedProductIds)) {
return true; // No specific products, process all
}
$orderItems = Arr::get($order, 'order_items', []);
// Post IDs of ordered products are the product IDs in FluentCart
$orderedProductIds = [];
foreach ($orderItems as $item) {
$productId = $item->post_id;
if ($productId) {
$orderedProductIds[] = $productId;
}
}
$productMatch = !empty(array_intersect($selectedProductIds, $orderedProductIds));
return $productMatch; // Return true if any of the selected products match the ordered products
}
}
@@ -0,0 +1,183 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers;
use FluentCrm\App\Services\Funnel\BaseTrigger;
use FluentCrm\App\Services\Funnel\FunnelHelper;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\CartHelper;
class SubscriptionCancelledTrigger extends BaseTrigger
{
public function __construct()
{
$this->triggerName = 'fluent_cart/subscription_canceled';
$this->priority = 20;
$this->actionArgNum = 1;
parent::__construct();
}
public function getTrigger()
{
return [
'ribbon' => 'subscription',
'category' => __('FluentCart', 'fluent-crm'),
'label' => __('Subscription Cancelled', 'fluent-crm'),
'description' => __('This will start when a subscription is cancelled', 'fluent-crm'),
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1"><g id="surface1"><path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 2.398438 0 L 21.601562 0 C 22.925781 0 24 1.074219 24 2.398438 L 24 21.601562 C 24 22.925781 22.925781 24 21.601562 24 L 2.398438 24 C 1.074219 24 0 22.925781 0 21.601562 L 0 2.398438 C 0 1.074219 1.074219 0 2.398438 0 Z M 2.398438 0 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 10.925781 16.476562 L 3.769531 16.476562 L 4.894531 13.878906 C 5.222656 13.117188 5.972656 12.625 6.804688 12.625 L 15.328125 12.625 L 14.746094 13.964844 C 14.085938 15.488281 12.585938 16.476562 10.925781 16.476562 Z M 10.925781 16.476562 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 16.851562 11.394531 L 6.789062 11.394531 L 7.367188 10.054688 C 8.027344 8.53125 9.53125 7.542969 11.191406 7.542969 L 19.886719 7.542969 L 18.761719 10.140625 C 18.433594 10.902344 17.683594 11.394531 16.851562 11.394531 Z M 16.851562 11.394531 "/></g></svg>'
];
}
public function getFunnelSettingsDefaults()
{
return [
'subscription_status' => 'subscribed',
];
}
public function getSettingsFields($funnel)
{
$statuses = fluentcrm_subscriber_editable_statuses(true);
return [
'title' => __('Subscription Cancelled', 'fluent-crm'),
'sub_title' => __('This will start when a subscription is cancelled', 'fluent-crm'),
'fields' => [
'subscription_status' => [
'type' => 'select',
'options' => $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 getConditionFields($funnel)
{
return [
'product_ids' => [
'type' => 'rest_selector',
'label' => __('Target Products (Subscription Only)', 'fluent-crm'),
'option_key' => 'fluent_cart_subscription_products',
'is_multiple' => true,
'help' => __('Select the products you want to include in the automation.', 'fluent-crm'),
'inline_help' => __('You can select multiple products. If you want to run for all products, then leave it empty', '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 enabled, it will restart the automation for a contact if the contact is already in the automation. Otherwise, it will skip if it already exists', 'fluent-crm'),
],
];
}
public function getFunnelConditionDefaults($funnel)
{
return [
'product_ids' => [],
'run_multiple' => 'no'
];
}
public function handle($funnel, $originalArgs)
{
$subscriptionData = $originalArgs[0];
$subscription = $subscriptionData['subscription'];
$order = $subscriptionData['order'];
$customer = $subscriptionData['customer'];
$subscriberData = CartHelper::prepareSubscriberData($customer);
if (!is_email($subscriberData['email'])) {
return;
}
$willProcess = $this->isProcessable($funnel, $order, $subscriberData);
if (!$willProcess) {
return;
}
$subscriberData = wp_parse_args($subscriberData, $funnel->settings);
$subscriberData['status'] = $subscriberData['subscription_status'];
unset($subscriberData['subscription_status']);
(new \FluentCrm\App\Services\Funnel\FunnelProcessor())->startFunnelSequence($funnel, $subscriberData, [
'source_trigger_name' => $this->triggerName,
'source_ref_id' => $order->id, // optional
]);
}
public function isProcessable($funnel, $order, $subscriberData)
{
$conditions = Arr::get($funnel, 'conditions', []);
$isProcessable = $this->checkConditions($conditions, $order, $subscriberData);
if(!$isProcessable){
return false;
}
$subscriber = FunnelHelper::getSubscriber($subscriberData['email']);
// check run_only_one
if ($subscriber) {
$funnelSub = FunnelHelper::ifAlreadyInFunnel($funnel->id, $subscriber->id);
if ($funnelSub) {
$multipleRun = Arr::get($conditions, 'run_multiple') == 'yes';
if ($multipleRun) {
if ($funnelSub->source_ref_id == $order->id) {
return false;
}
FunnelHelper::removeSubscribersFromFunnel($funnel->id, [$subscriber->id]);
}
return $multipleRun;
}
}
return true;
}
private function checkConditions($conditions, $order, $subscriber)
{
$selectedProductIds = Arr::get($conditions, 'product_ids', []);
if (empty($selectedProductIds)) {
return true; // No specific products, process all
}
$orderItems = Arr::get($order, 'order_items', []);
// Post IDs of ordered products are the product IDs in FluentCart
$orderedProductIds = [];
foreach ($orderItems as $item) {
$productId = $item->post_id;
if ($productId) {
$orderedProductIds[] = $productId;
}
}
$productMatch = !empty(array_intersect($selectedProductIds, $orderedProductIds));
return $productMatch; // Return true if any of the selected products match the ordered products
}
}
@@ -0,0 +1,183 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers;
use FluentCrm\App\Services\Funnel\BaseTrigger;
use FluentCrm\App\Services\Funnel\FunnelHelper;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\CartHelper;
class SubscriptionEndOfTermTrigger extends BaseTrigger
{
public function __construct()
{
$this->triggerName = 'fluent_cart/subscription_eot';
$this->priority = 20;
$this->actionArgNum = 1;
parent::__construct();
}
public function getTrigger()
{
return [
'ribbon' => 'subscription',
'category' => __('FluentCart', 'fluent-crm'),
'label' => __('Subscription End of Term (Completed)', 'fluent-crm'),
'description' => __('This will start when a subscription reaches it\'s end of term(completed)', 'fluent-crm'),
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1"><g id="surface1"><path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 2.398438 0 L 21.601562 0 C 22.925781 0 24 1.074219 24 2.398438 L 24 21.601562 C 24 22.925781 22.925781 24 21.601562 24 L 2.398438 24 C 1.074219 24 0 22.925781 0 21.601562 L 0 2.398438 C 0 1.074219 1.074219 0 2.398438 0 Z M 2.398438 0 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 10.925781 16.476562 L 3.769531 16.476562 L 4.894531 13.878906 C 5.222656 13.117188 5.972656 12.625 6.804688 12.625 L 15.328125 12.625 L 14.746094 13.964844 C 14.085938 15.488281 12.585938 16.476562 10.925781 16.476562 Z M 10.925781 16.476562 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 16.851562 11.394531 L 6.789062 11.394531 L 7.367188 10.054688 C 8.027344 8.53125 9.53125 7.542969 11.191406 7.542969 L 19.886719 7.542969 L 18.761719 10.140625 C 18.433594 10.902344 17.683594 11.394531 16.851562 11.394531 Z M 16.851562 11.394531 "/></g></svg>'
];
}
public function getFunnelSettingsDefaults()
{
return [
'subscription_status' => 'subscribed',
];
}
public function getSettingsFields($funnel)
{
$statuses = fluentcrm_subscriber_editable_statuses(true);
return [
'title' => __('Subscription End of Term (Completed)', 'fluent-crm'),
'sub_title' => __('This will start when a subscription reaches it\'s end of term(completed)', 'fluent-crm'),
'fields' => [
'subscription_status' => [
'type' => 'select',
'options' => $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 getConditionFields($funnel)
{
return [
'product_ids' => [
'type' => 'rest_selector',
'label' => __('Target Products (Subscription Only)', 'fluent-crm'),
'option_key' => 'fluent_cart_subscription_products',
'is_multiple' => true,
'help' => __('Select the products you want to include in the automation.', 'fluent-crm'),
'inline_help' => __('You can select multiple products. If you want to run for all products, then leave it empty', '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 enabled, it will restart the automation for a contact if the contact is already in the automation. Otherwise, it will skip if it already exists', 'fluent-crm'),
],
];
}
public function getFunnelConditionDefaults($funnel)
{
return [
'product_ids' => [],
'run_multiple' => 'no'
];
}
public function handle($funnel, $originalArgs)
{
$subscriptionData = $originalArgs[0];
$subscription = $subscriptionData['subscription'];
$order = $subscriptionData['order'];
$customer = $subscriptionData['customer'];
$subscriberData = CartHelper::prepareSubscriberData($customer);
if (!is_email($subscriberData['email'])) {
return;
}
$willProcess = $this->isProcessable($funnel, $order, $subscriberData);
if (!$willProcess) {
return;
}
$subscriberData = wp_parse_args($subscriberData, $funnel->settings);
$subscriberData['status'] = $subscriberData['subscription_status'];
unset($subscriberData['subscription_status']);
(new \FluentCrm\App\Services\Funnel\FunnelProcessor())->startFunnelSequence($funnel, $subscriberData, [
'source_trigger_name' => $this->triggerName,
'source_ref_id' => $order->id, // optional
]);
}
public function isProcessable($funnel, $order, $subscriberData)
{
$conditions = Arr::get($funnel, 'conditions', []);
$isProcessable = $this->checkConditions($conditions, $order, $subscriberData);
if(!$isProcessable){
return false;
}
$subscriber = FunnelHelper::getSubscriber($subscriberData['email']);
// check run_only_one
if ($subscriber) {
$funnelSub = FunnelHelper::ifAlreadyInFunnel($funnel->id, $subscriber->id);
if ($funnelSub) {
$multipleRun = Arr::get($conditions, 'run_multiple') == 'yes';
if ($multipleRun) {
if ($funnelSub->source_ref_id == $order->id) {
return false;
}
FunnelHelper::removeSubscribersFromFunnel($funnel->id, [$subscriber->id]);
}
return $multipleRun;
}
}
return true;
}
private function checkConditions($conditions, $order, $subscriber)
{
$selectedProductIds = Arr::get($conditions, 'product_ids', []);
if (empty($selectedProductIds)) {
return true; // No specific products, process all
}
$orderItems = Arr::get($order, 'order_items', []);
// Post IDs of ordered products are the product IDs in FluentCart
$orderedProductIds = [];
foreach ($orderItems as $item) {
$productId = $item->post_id;
if ($productId) {
$orderedProductIds[] = $productId;
}
}
$productMatch = !empty(array_intersect($selectedProductIds, $orderedProductIds));
return $productMatch; // Return true if any of the selected products match the ordered products
}
}
@@ -0,0 +1,184 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers;
use FluentCrm\App\Services\Funnel\BaseTrigger;
use FluentCrm\App\Services\Funnel\FunnelHelper;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\CartHelper;
class SubscriptionExpiredTrigger extends BaseTrigger
{
public function __construct()
{
$this->triggerName = 'fluent_cart/subscription_expired_validity';
$this->priority = 20;
$this->actionArgNum = 1;
parent::__construct();
}
public function getTrigger()
{
return [
'ribbon' => 'subscription',
'category' => __('FluentCart', 'fluent-crm'),
'label' => __('Subscription Expired / End of Access Validity', 'fluent-crm'),
'description' => __('This will start when a subscription expires', 'fluent-crm'),
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1"><g id="surface1"><path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 2.398438 0 L 21.601562 0 C 22.925781 0 24 1.074219 24 2.398438 L 24 21.601562 C 24 22.925781 22.925781 24 21.601562 24 L 2.398438 24 C 1.074219 24 0 22.925781 0 21.601562 L 0 2.398438 C 0 1.074219 1.074219 0 2.398438 0 Z M 2.398438 0 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 10.925781 16.476562 L 3.769531 16.476562 L 4.894531 13.878906 C 5.222656 13.117188 5.972656 12.625 6.804688 12.625 L 15.328125 12.625 L 14.746094 13.964844 C 14.085938 15.488281 12.585938 16.476562 10.925781 16.476562 Z M 10.925781 16.476562 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 16.851562 11.394531 L 6.789062 11.394531 L 7.367188 10.054688 C 8.027344 8.53125 9.53125 7.542969 11.191406 7.542969 L 19.886719 7.542969 L 18.761719 10.140625 C 18.433594 10.902344 17.683594 11.394531 16.851562 11.394531 Z M 16.851562 11.394531 "/></g></svg>'
];
}
public function getFunnelSettingsDefaults()
{
return [
'subscription_status' => 'subscribed',
];
}
public function getSettingsFields($funnel)
{
$statuses = fluentcrm_subscriber_editable_statuses(true);
return [
'title' => __('Subscription Expired / End of Access Validity', 'fluent-crm'),
'sub_title' => __('This will start when a subscription expires', 'fluent-crm'),
'fields' => [
'subscription_status' => [
'type' => 'select',
'options' => $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 getConditionFields($funnel)
{
return [
'product_ids' => [
'type' => 'rest_selector',
'label' => __('Target Products (Subscription Only)', 'fluent-crm'),
'option_key' => 'fluent_cart_subscription_products',
'is_multiple' => true,
'help' => __('Select the products you want to include in the automation.', 'fluent-crm'),
'inline_help' => __('You can select multiple products. If you want to run for all products, then leave it empty', '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 enabled, it will restart the automation for a contact if the contact is already in the automation. Otherwise, it will skip if it already exists', 'fluent-crm'),
],
];
}
public function getFunnelConditionDefaults($funnel)
{
return [
'product_ids' => [],
'run_multiple' => 'no'
];
}
public function handle($funnel, $originalArgs)
{
$subscriptionData = $originalArgs[0];
$subscription = $subscriptionData['subscription'];
$order = $subscriptionData['order'];
$customer = $subscriptionData['customer'];
// $oldStatus = $subscriptionData['old_status'];
// $newStatus = $subscription->status;
$subscriberData = CartHelper::prepareSubscriberData($customer);
if (!is_email($subscriberData['email'])) {
return;
}
$willProcess = $this->isProcessable($funnel, $order, $subscriberData);
if (!$willProcess) {
return;
}
$subscriberData = wp_parse_args($subscriberData, $funnel->settings);
$subscriberData['status'] = $subscriberData['subscription_status'];
unset($subscriberData['subscription_status']);
(new \FluentCrm\App\Services\Funnel\FunnelProcessor())->startFunnelSequence($funnel, $subscriberData, [
'source_trigger_name' => $this->triggerName,
'source_ref_id' => $order->id, // optional
]);
}
public function isProcessable($funnel, $order, $subscriberData)
{
$conditions = Arr::get($funnel, 'conditions', []);
$isProcessable = $this->checkConditions($conditions, $order, $subscriberData);
if(!$isProcessable){
return false;
}
$subscriber = FunnelHelper::getSubscriber($subscriberData['email']);
// check run_only_one
if ($subscriber) {
$funnelSub = FunnelHelper::ifAlreadyInFunnel($funnel->id, $subscriber->id);
if ($funnelSub) {
$multipleRun = Arr::get($conditions, 'run_multiple') == 'yes';
if ($multipleRun) {
if ($funnelSub->source_ref_id == $order->id) {
return false;
}
FunnelHelper::removeSubscribersFromFunnel($funnel->id, [$subscriber->id]);
}
return $multipleRun;
}
}
return true;
}
private function checkConditions($conditions, $order, $subscriber)
{
$selectedProductIds = Arr::get($conditions, 'product_ids', []);
if (empty($selectedProductIds)) {
return true; // No specific products, process all
}
$orderItems = Arr::get($order, 'order_items', []);
// Post IDs of ordered products are the product IDs in FluentCart
$orderedProductIds = [];
foreach ($orderItems as $item) {
$productId = $item->post_id;
if ($productId) {
$orderedProductIds[] = $productId;
}
}
$productMatch = !empty(array_intersect($selectedProductIds, $orderedProductIds));
return $productMatch; // Return true if any of the selected products match the ordered products
}
}
@@ -0,0 +1,183 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentCart\Triggers;
use FluentCrm\App\Services\Funnel\BaseTrigger;
use FluentCrm\App\Services\Funnel\FunnelHelper;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\App\Services\ExternalIntegrations\FluentCart\CartHelper;
class SubscriptionRenewedTrigger extends BaseTrigger
{
public function __construct()
{
$this->triggerName = 'fluent_cart/subscription_renewed';
$this->priority = 20;
$this->actionArgNum = 1;
parent::__construct();
}
public function getTrigger()
{
return [
'ribbon' => 'subscription',
'category' => __('FluentCart', 'fluent-crm'),
'label' => __('Subscription Renewed', 'fluent-crm'),
'description' => __('This will start when a subscription is renewed', 'fluent-crm'),
'svg' => '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24px" height="24px" viewBox="0 0 24 24" version="1.1"><g id="surface1"><path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 2.398438 0 L 21.601562 0 C 22.925781 0 24 1.074219 24 2.398438 L 24 21.601562 C 24 22.925781 22.925781 24 21.601562 24 L 2.398438 24 C 1.074219 24 0 22.925781 0 21.601562 L 0 2.398438 C 0 1.074219 1.074219 0 2.398438 0 Z M 2.398438 0 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 10.925781 16.476562 L 3.769531 16.476562 L 4.894531 13.878906 C 5.222656 13.117188 5.972656 12.625 6.804688 12.625 L 15.328125 12.625 L 14.746094 13.964844 C 14.085938 15.488281 12.585938 16.476562 10.925781 16.476562 Z M 10.925781 16.476562 "/><path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,62.352943%);fill-opacity:1;" d="M 16.851562 11.394531 L 6.789062 11.394531 L 7.367188 10.054688 C 8.027344 8.53125 9.53125 7.542969 11.191406 7.542969 L 19.886719 7.542969 L 18.761719 10.140625 C 18.433594 10.902344 17.683594 11.394531 16.851562 11.394531 Z M 16.851562 11.394531 "/></g></svg>'
];
}
public function getFunnelSettingsDefaults()
{
return [
'subscription_status' => 'subscribed',
];
}
public function getSettingsFields($funnel)
{
$statuses = fluentcrm_subscriber_editable_statuses(true);
return [
'title' => __('Subscription Renewed', 'fluent-crm'),
'sub_title' => __('This will start when a subscription is renewed', 'fluent-crm'),
'fields' => [
'subscription_status' => [
'type' => 'select',
'options' => $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 getConditionFields($funnel)
{
return [
'product_ids' => [
'type' => 'rest_selector',
'label' => __('Target Products (Subscription Only)', 'fluent-crm'),
'option_key' => 'fluent_cart_subscription_products',
'is_multiple' => true,
'help' => __('Select the products you want to include in the automation.', 'fluent-crm'),
'inline_help' => __('You can select multiple products. If you want to run for all products, then leave it empty', '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 enabled, it will restart the automation for a contact if the contact is already in the automation. Otherwise, it will skip if it already exists', 'fluent-crm'),
],
];
}
public function getFunnelConditionDefaults($funnel)
{
return [
'product_ids' => [],
'run_multiple' => 'no'
];
}
public function handle($funnel, $originalArgs)
{
$subscriptionData = $originalArgs[0];
$subscription = $subscriptionData['subscription'];
$order = $subscriptionData['order'];
$customer = $subscriptionData['customer'];
$subscriberData = CartHelper::prepareSubscriberData($customer);
if (!is_email($subscriberData['email'])) {
return;
}
$willProcess = $this->isProcessable($funnel, $order, $subscriberData);
if (!$willProcess) {
return;
}
$subscriberData = wp_parse_args($subscriberData, $funnel->settings);
$subscriberData['status'] = $subscriberData['subscription_status'];
unset($subscriberData['subscription_status']);
(new \FluentCrm\App\Services\Funnel\FunnelProcessor())->startFunnelSequence($funnel, $subscriberData, [
'source_trigger_name' => $this->triggerName,
'source_ref_id' => $order->id, // optional
]);
}
public function isProcessable($funnel, $order, $subscriberData)
{
$conditions = Arr::get($funnel, 'conditions', []);
$isProcessable = $this->checkConditions($conditions, $order, $subscriberData);
if(!$isProcessable){
return false;
}
$subscriber = FunnelHelper::getSubscriber($subscriberData['email']);
// check run_only_one
if ($subscriber) {
$funnelSub = FunnelHelper::ifAlreadyInFunnel($funnel->id, $subscriber->id);
if ($funnelSub) {
$multipleRun = Arr::get($conditions, 'run_multiple') == 'yes';
if ($multipleRun) {
if ($funnelSub->source_ref_id == $order->id) {
return false;
}
FunnelHelper::removeSubscribersFromFunnel($funnel->id, [$subscriber->id]);
}
return $multipleRun;
}
}
return true;
}
private function checkConditions($conditions, $order, $subscriber)
{
$selectedProductIds = Arr::get($conditions, 'product_ids', []);
if (empty($selectedProductIds)) {
return true; // No specific products, process all
}
$orderItems = Arr::get($order, 'order_items', []);
// Post IDs of ordered products are the product IDs in FluentCart
$orderedProductIds = [];
foreach ($orderItems as $item) {
$productId = $item->post_id;
if ($productId) {
$orderedProductIds[] = $productId;
}
}
$productMatch = !empty(array_intersect($selectedProductIds, $orderedProductIds));
return $productMatch; // Return true if any of the selected products match the ordered products
}
}
@@ -0,0 +1,659 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentForm;
use FluentCrm\App\Models\CustomContactField;
use FluentCrm\App\Models\Lists;
use FluentCrm\App\Models\Subscriber;
use FluentCrm\App\Models\Tag;
use FluentCrm\App\Services\Funnel\FunnelHelper;
use FluentCrm\Framework\Support\Arr;
use FluentForm\App\Http\Controllers\IntegrationManagerController;
use FluentForm\App\Modules\Form\FormFieldsParser;
use FluentForm\App\Services\FormBuilder\ShortCodeParser;
use FluentForm\App\Services\Integrations\GlobalNotificationService;
use FluentForm\Framework\Helpers\ArrayHelper;
class Bootstrap extends IntegrationManagerController
{
public $hasGlobalMenu = false;
public $disableGlobalSettings = 'yes';
public function __construct()
{
parent::__construct(
null,
__('FluentCRM', 'fluent-crm'),
'fluentcrm',
'_fluentform_fluentcrm_settings',
'fluentcrm_feeds',
10
);
$this->logo = FLUENTCRM_PLUGIN_URL . 'assets/images/fluentcrm-logo.svg';
$this->description = __('Connect FluentCRM with WP Fluent Forms and subscribe a contact when a form is submitted.', 'fluent-crm');
$this->registerAdminHooks();
add_filter('fluentform/notifying_async_fluentcrm', '__return_false');
$this->registerPaymentEvents();
}
public function pushIntegration($integrations, $formId)
{
$integrations[$this->integrationKey] = [
'title' => $this->title . ' Integration',
'logo' => $this->logo,
'is_active' => $this->isConfigured(),
'configure_title' => __('Configuration required!', 'fluent-crm'),
'global_configure_url' => '#',
'configure_message' => __('FluentCRM is not configured yet! Please configure your FluentCRM API first', 'fluent-crm'),
'configure_button_text' => __('Set FluentCRM', 'fluent-crm')
];
return $integrations;
}
public function getIntegrationDefaults($settings, $formId)
{
return [
'name' => '',
'first_name' => '',
'last_name' => '',
'full_name' => '',
'email' => '',
'other_fields' => [
[
'item_value' => '',
'label' => ''
]
],
'list_id' => '',
'tag_ids' => [],
'tag_ids_selection_type' => 'simple',
'tag_routers' => [],
'skip_if_exists' => false,
'double_opt_in' => false,
'force_subscribe' => false,
'skip_primary_data' => false,
'conditionals' => [
'conditions' => [],
'status' => false,
'type' => 'all'
],
'run_events_only' => [],
'remove_tags' => [],
'enabled' => true
];
}
public function getSettingsFields($settings, $formId)
{
$form = fluentFormApi('forms')->find($formId);
$paymentFields = FormFieldsParser::getPaymentFields($form, ['element']);
$fieldOptions = [];
foreach (Subscriber::mappables() as $key => $column) {
$fieldOptions[$key] = $column;
}
foreach ((new CustomContactField)->getGlobalFields()['fields'] as $field) {
$fieldOptions[$field['slug']] = $field['label'];
}
$fieldOptions['avatar'] = 'Profile Photo';
unset($fieldOptions['email']);
unset($fieldOptions['first_name']);
unset($fieldOptions['last_name']);
$fields = [
[
'key' => 'name',
'label' => __('Feed Name', 'fluent-crm'),
'required' => true,
'placeholder' => __('Your Feed Name', 'fluent-crm'),
'component' => 'text'
],
[
'key' => 'list_id',
'label' => __('FluentCRM List', 'fluent-crm'),
'placeholder' => __('Select FluentCRM List', 'fluent-crm'),
'tips' => __('Select the FluentCRM List you would like to add your contacts to.', 'fluent-crm'),
'component' => 'select',
'required' => false,
'options' => $this->getLists(),
],
[
'key' => 'CustomFields',
'require_list' => false,
'label' => __('Primary Fields', 'fluent-crm'),
'tips' => __('Associate your FluentCRM merge tags to the appropriate Fluent Form fields by selecting the appropriate form field from the list.', 'fluent-crm'),
'component' => 'map_fields',
'field_label_remote' => __('FluentCRM Field', 'fluent-crm'),
'field_label_local' => __('Form Field', 'fluent-crm'),
'primary_fileds' => [
[
'key' => 'email',
'label' => __('Email Address', 'fluent-crm'),
'required' => true,
'input_options' => 'emails'
],
[
'key' => 'first_name',
'label' => __('First Name', 'fluent-crm')
],
[
'key' => 'last_name',
'label' => __('Last Name', 'fluent-crm')
],
[
'key' => 'full_name',
'label' => __('Full Name', 'fluent-crm'),
'help_text' => __('If First Name & Last Name is not available full name will be used to get first name and last name', 'fluent-crm')
]
]
],
[
'key' => 'other_fields',
'require_list' => false,
'label' => __('Other Fields', 'fluent-crm'),
'tips' => __('Select which Fluent Form fields pair with their<br /> respective FluentCRM fields.', 'fluent-crm'),
'component' => 'dropdown_many_fields',
'field_label_remote' => __('FluentCRM Field', 'fluent-crm'),
'field_label_local' => __('Form Field', 'fluent-crm'),
'options' => $fieldOptions
],
[
'key' => 'tag_ids',
'require_list' => false,
'label' => __('Contact Tags', 'fluent-crm'),
'placeholder' => __('Select Tags', 'fluent-crm'),
'component' => 'selection_routing',
'simple_component' => 'select',
'routing_input_type' => 'select',
'routing_key' => 'tag_ids_selection_type',
'settings_key' => 'tag_routers',
'is_multiple' => true,
'labels' => [
'choice_label' => __('Enable Dynamic Tag Selection', 'fluent-crm'),
'input_label' => '',
'input_placeholder' => __('Set Tag', 'fluent-crm')
],
'options' => $this->getTags()
],
[
'key' => 'skip_if_exists',
'require_list' => false,
'checkbox_label' => __('Skip if contact already exists in FluentCRM', 'fluent-crm'),
'component' => 'checkbox-single'
],
[
'key' => 'skip_primary_data',
'require_list' => false,
'checkbox_label' => __('Skip name update if an existing contact has old data (per primary field)', 'fluent-crm'),
'component' => 'checkbox-single'
],
[
'key' => 'double_opt_in',
'require_list' => false,
'checkbox_label' => __('Enable Double opt-in for new contacts', 'fluent-crm'),
'component' => 'checkbox-single'
],
[
'key' => 'force_subscribe',
'require_list' => false,
'checkbox_label' => __('Enable Force Subscribe if contact is not in subscribed status (Existing contact only)', 'fluent-crm'),
'component' => 'checkbox-single',
'inline_tip' => __('If you enable this, the contact will be forcefully subscribed regardless of the contact\'s current status', 'fluent-crm')
],
[
'require_list' => false,
'key' => 'conditionals',
'label' => __('Conditional Logics', 'fluent-crm'),
'tips' => __('Allow FluentCRM integration conditionally based on your submission values', 'fluent-crm'),
'component' => 'conditional_block'
]
];
if ($paymentFields) {
$hasSubscriptionFields = !!FormFieldsParser::getInputsByElementTypes($form, ['subscription_payment_component']);
$options = [
'fluentform/payment_refunded' => __('On Payment Refund', 'fluent-crm')
];
if ($hasSubscriptionFields) {
$options = [
'fluentform/subscription_payment_active' => __('On Subscription Active', 'fluent-crm'),
'fluentform/subscription_payment_canceled' => __('On Subscription Cancel', 'fluent-crm'),
'fluentform/payment_refunded' => __('On Payment Refund', 'fluent-crm')
];
}
$fields[] = [
'require_list' => false,
'key' => 'run_events_only',
'label' => __('Run only on events', 'fluent-crm'),
'component' => 'checkbox-multiple-text',
'options' => $options,
'tips' => __('If you check any of the events, then this feed will only run for the selected events', 'fluent-crm')
];
}
$fields[] = [
'require_list' => false,
'key' => 'remove_tags',
'label' => __('Remove Contact Tags', 'fluent-crm'),
'placeholder' => __('Select Tags (remove from contact)', 'fluent-crm'),
'tips' => __('(Optional) The selected tags will be removed from the contact (if it exists)', 'fluent-crm'),
'component' => 'select',
'is_multiple' => true,
'required' => false,
'options' => $this->getTags(),
];
$fields[] = [
'require_list' => false,
'key' => 'enabled',
'label' => __('Status', 'fluent-crm'),
'component' => 'checkbox-single',
'checkbox_label' => __('Enable This feed', 'fluent-crm')
];
return [
'fields' => $fields,
'button_require_list' => false,
'integration_title' => $this->title
];
}
public function getMergeFields($list, $listId, $formId)
{
return [];
}
protected function getLists()
{
$lists = Lists::orderBy('title', 'ASC')->get();
$formattedLists = [];
foreach ($lists as $list) {
$formattedLists[$list->id] = $list->title;
}
return $formattedLists;
}
protected function getTags()
{
$tags = Tag::orderBy('title', 'ASC')->get();
$formattedTags = [];
foreach ($tags as $tag) {
$formattedTags[strval($tag->id)] = $tag->title;
}
return $formattedTags;
}
/*
* Form Submission Hooks Here
*/
public function notify($feed, $formData, $entry, $form)
{
// check if only on payment event
if (Arr::get($feed, 'settings.run_events_only')) {
// We have running events selected. So we may not run this feed.
$paymentFields = FormFieldsParser::getPaymentFields($form, ['element']);
if ($paymentFields) {
return false;
}
}
return $this->runFeed($feed, $formData, $entry, $form);
}
private function runFeed($feed, $formData, $entry, $form)
{
$data = $feed['processedValues'];
$contact = Arr::only($data, ['first_name', 'last_name', 'email']);
if (!is_email($contact['email'])) {
$contact['email'] = ArrayHelper::get($formData, $contact['email']);
}
if (!$contact['first_name'] && !$contact['last_name']) {
$fullName = Arr::get($data, 'full_name');
if ($fullName) {
$nameArray = explode(' ', $fullName);
if (count($nameArray) > 1) {
$contact['last_name'] = array_pop($nameArray);
$contact['first_name'] = implode(' ', $nameArray);
} else {
$contact['first_name'] = $fullName;
}
}
}
foreach (Arr::get($data, 'other_fields') as $field) {
if ($field['item_value']) {
$contact[$field['label']] = str_replace('<br />', ' ', $field['item_value']);
}
}
if ($entry->ip) {
$contact['ip'] = $entry->ip;
}
if (!is_email($contact['email'])) {
$this->addLog(
$feed['settings']['name'],
'failed',
__('FluentCRM API called skipped because no valid email available', 'fluent-crm'),
$form->id,
$entry->id
);
return false;
}
if (isset($contact['country'])) {
$country = FunnelHelper::getCountryShortName($contact['country']);
if ($country) {
$contact['country'] = $country;
} else {
unset($contact['country']);
}
}
$subscriber = Subscriber::where('email', $contact['email'])->first();
if ($subscriber && Arr::isTrue($data, 'skip_if_exists')) {
$this->addLog(
$feed['settings']['name'],
'info',
__('Contact creation has been skipped because the contact already exists in the database', 'fluent-crm'),
$form->id,
$entry->id
);
return false;
}
if (!empty($contact['avatar'])) {
// validate the avatar photo
$validUrl = '';
if (filter_var($contact['avatar'], FILTER_VALIDATE_URL) !== FALSE) {
$url = $contact['avatar'];
$dots = explode('.', $url);
$ext = strtolower(end($dots));
if (in_array($ext, ['png', 'jpg', 'jpeg', 'webp', 'gif'])) {
$validUrl = $contact['avatar'];
}
}
if (!$validUrl) {
unset($contact['avatar']);
}
}
if ($subscriber) {
if ($subscriber->ip && isset($contact['ip'])) {
unset($contact['ip']);
}
if (Arr::isTrue($data, 'skip_primary_data')) {
if ($subscriber->first_name) {
unset($contact['first_name']);
unset($contact['last_name']);
}
}
}
$user = get_user_by('email', $contact['email']);
if ($user) {
$contact['user_id'] = $user->ID;
}
$tags = $this->getSelectedTagIds($data, $formData, 'tag_ids');
if ($tags) {
$contact['tags'] = $tags;
}
if (!$subscriber) {
if (empty($contact['source'])) {
$contact['source'] = 'FluentForms';
}
if (Arr::isTrue($data, 'double_opt_in')) {
$contact['status'] = 'pending';
} else {
$contact['status'] = 'subscribed';
}
if ($listId = Arr::get($data, 'list_id')) {
$contact['lists'] = [$listId];
}
$subscriber = FluentCrmApi('contacts')->createOrUpdate($contact, false, false);
if (!$subscriber) {
return false;
}
if ($entry->status == 'confirmed' && $subscriber->status != 'subscribed') {
$subscriber = $subscriber->updateStatus('subscribed');
}
if ($subscriber->status == 'pending') {
$subscriber->sendDoubleOptinEmail();
}
$this->addLog(
$feed['settings']['name'],
'success',
__('Contact has been created in FluentCRM. Contact ID: ', 'fluent-crm') . $subscriber->id,
$form->id,
$entry->id
);
do_action('fluent_crm/contact_added_by_fluentform', $subscriber, $entry, $form, $feed);
} else {
if ($listId = Arr::get($data, 'list_id')) {
$contact['lists'] = [$listId];
}
$hasDouBleOptIn = Arr::isTrue($data, 'double_opt_in');
$forceSubscribed = !$hasDouBleOptIn && ($subscriber->status != 'subscribed');
if (!$forceSubscribed) {
$forceSubscribed = Arr::isTrue($data, 'force_subscribe');
}
if ($forceSubscribed) {
$contact['status'] = 'subscribed';
}
$originalFields = FluentCrmApi('contacts')->getContact($contact['email'])->getOriginal();
$currentCustomValues = $subscriber->custom_fields();
$subscriber = FluentCrmApi('contacts')->createOrUpdate($contact, $forceSubscribed, false);
if (!$subscriber) {
return false;
}
if ($entry->status == 'confirmed' && $subscriber->status != 'subscribed') {
$subscriber = $subscriber->updateStatus('subscribed');
}
if ($hasDouBleOptIn && ($subscriber->status == 'pending' || $subscriber->status == 'unsubscribed')) {
$subscriber->sendDoubleOptinEmail();
}
do_action('fluent_crm/contact_updated_by_fluentform', $subscriber, $entry, $form, $feed);
if ($removeTags = Arr::get($feed, 'settings.remove_tags', [])) {
$subscriber->detachTags($removeTags);
}
$dirtyFields = array_merge($feed['processedValues'], ['dirty_custom_fields' => $currentCustomValues]);
do_action('fluent_crm/contact_updated_with_changes', $subscriber, $dirtyFields, $originalFields, ['source' => 'fluentform', 'formId' => $form->id]);
$this->addLog(
$feed['settings']['name'],
'success',
__('Contact has been updated in FluentCRM. Contact ID: ', 'fluent-crm') . $subscriber->id,
$form->id,
$entry->id
);
}
}
public function isConfigured()
{
return true;
}
public function isEnabled()
{
return true;
}
protected function addLog($title, $status, $description, $formId, $entryId)
{
do_action('ff_log_data', [
'title' => $title,
'status' => $status,
'description' => $description,
'parent_source_id' => $formId,
'source_id' => $entryId,
'component' => $this->integrationKey,
'source_type' => 'submission_item'
]);
}
/*
* We will remove this in future
*/
protected function getSelectedTagIds($data, $inputData, $simpleKey = 'tag_ids', $routingId = 'tag_ids_selection_type', $routersKey = 'tag_routers')
{
$routing = ArrayHelper::get($data, $routingId, 'simple');
if (!$routing || $routing == 'simple') {
return ArrayHelper::get($data, $simpleKey, []);
}
$routers = ArrayHelper::get($data, $routersKey);
if (empty($routers)) {
return [];
}
return $this->evaluateRoutings($routers, $inputData);
}
/*
* We will remove this in future
*/
protected function evaluateRoutings($routings, $inputData)
{
$validInputs = [];
foreach ($routings as $routing) {
$inputValue = ArrayHelper::get($routing, 'input_value');
if (!$inputValue) {
continue;
}
$condition = [
'conditionals' => [
'status' => true,
'is_test' => true,
'type' => 'any',
'conditions' => [
$routing
]
]
];
if (\FluentForm\App\Services\ConditionAssesor::evaluate($condition, $inputData)) {
$validInputs[] = $inputValue;
}
}
return $validInputs;
}
private function registerPaymentEvents()
{
add_action('fluentform/subscription_payment_active', function ($subscription, $submission) {
$this->handlePaymentEvent($submission, 'fluentform/subscription_payment_active');
}, 10, 2);
add_action('fluentform/subscription_payment_canceled', function ($subscription, $submission) {
$this->handlePaymentEvent($submission, 'fluentform/subscription_payment_canceled');
}, 10, 2);
add_action('fluentform/payment_refunded', function ($refund, $transaction, $submission) {
$this->handlePaymentEvent($submission, 'fluentform/payment_refunded');
}, 10, 3);
}
private function handlePaymentEvent($submission, $event)
{
// Get Fluent Forms Feeds
$feeds = fluentCrmDb()->table('fluentform_form_meta')
->where('form_id', $submission->form_id)
->where('meta_key', 'fluentcrm_feeds')
->orderBy('id', 'ASC')
->get();
if ($feeds->isEmpty()) {
return false;
}
if (!is_array($submission->response)) {
$formData = json_decode($submission->response, true);
} else {
$formData = $submission->response;
}
$form = fluentFormApi('forms')->find($submission->form_id);
$notificationService = new GlobalNotificationService();
foreach ($feeds as $feed) {
$parsedValue = json_decode($feed->value, true);
if ($parsedValue && ArrayHelper::isTrue($parsedValue, 'enabled')) {
$runEvents = ArrayHelper::get($parsedValue, 'run_events_only', []);
// check if this is our event or not
if (!$runEvents || !in_array($event, $runEvents)) {
continue;
}
// Now check if conditions matched or not
$isConditionMatched = $notificationService->checkCondition($parsedValue, $formData, $submission->id);
if ($isConditionMatched) {
$item = [
'id' => $feed->id,
'meta_key' => $feed->meta_key,
'settings' => $parsedValue
];
$processedValues = $item['settings'];
unset($processedValues['conditionals']);
$item['processedValues'] = ShortCodeParser::parse($processedValues, $submission->id, $formData, $form, false, $feed->meta_key);
$this->runFeed($item, $formData, $submission, $form);
}
}
}
}
}
@@ -0,0 +1,128 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\FluentForm;
use FluentCrm\Framework\Support\Arr;
use FluentForm\App\Helpers\Helper;
use FluentFormPro\Payments\PaymentHelper;
class FluentFormInit
{
public function init()
{
if (defined('FLUENTFORM_FRAMEWORK_UPGRADE')) {
new \FluentCrm\App\Services\ExternalIntegrations\FluentForm\Bootstrap();
}
add_filter('fluentform/submissions_widgets', array($this, 'pushContactWidget'), 10, 3);
if (defined('FLUENTFORMPRO')) {
add_filter('fluent_crm/subscriber_info_widgets', array($this, 'pushSubscriberInfoWidget'), 10, 2);
}
}
public function pushContactWidget($widgets, $resources, $submission)
{
$userId = $submission->user_id;
if (!$userId) {
$userInputs = json_decode($submission->response, true);
if (!$userInputs) {
return $widgets;
}
$maybeEmail = Arr::get($userInputs, 'email');
if (!$maybeEmail) {
$emailField = Helper::getFormMeta($submission->form_id, '_primary_email_field');
if (!$emailField) {
return $widgets;
}
$maybeEmail = Arr::get($userInputs, $emailField);
}
} else {
$maybeEmail = $userId;
}
if (!$maybeEmail) {
return $widgets;
}
$profileHtml = fluentcrm_get_crm_profile_html($maybeEmail, true);
if (!$profileHtml) {
return $widgets;
}
$widgets['fluent_crm'] = [
'title' => __('FluentCRM Profile', 'fluent-crm'),
'content' => $profileHtml
];
return $widgets;
}
public function pushSubscriberInfoWidget($widgets, $subscriber)
{
if(!$subscriber->email) {
return $widgets;
}
$subscriptions = fluentCrmDb()->table('fluentform_subscriptions')
->join('fluentform_transactions', 'fluentform_subscriptions.submission_id', '=', 'fluentform_transactions.submission_id')
->where('fluentform_transactions.payer_email', $subscriber->email)
->select('fluentform_subscriptions.*', 'fluentform_transactions.currency')
->orderBy('fluentform_subscriptions.created_at', 'desc')
->get();
if ($subscriptions->isEmpty()) {
return $widgets;
}
$html = '<ul class="fc_full_listed fc_memberpress_subscription_lists">';
foreach ($subscriptions as $subscription) {
$html .= $this->generateSubscriptionHtml($subscription);
}
$html .= '</ul>';
$widgets[] = [
'title' => __('FluentForm Subscriptions', 'fluent-crm'),
'content' => $html
];
return $widgets;
}
private function generateSubscriptionHtml($subscription)
{
$subscription->formatted_recurring_amount = PaymentHelper::formatMoney($subscription->recurring_amount, $subscription->currency);
if ($subscription->status == 'active') {
if ($subscription->bill_times) {
/* translators: 1: total number of payments, 2: current payment count, 3: total number of payments */
$billingText = sprintf(esc_html__('Will be cancelled after %1$d payments. (%2$d/%3$d)', 'fluent-crm'), $subscription->bill_times, $subscription->bill_count, $subscription->bill_times);
} else {
$billingText = __('Will be billed until cancelled', 'fluent-crm');
}
}
$formatted_date = date_i18n(get_option('date_format'), strtotime($subscription->created_at));
$permalink = esc_url(admin_url('admin.php?page=fluent_forms&form_id='.$subscription->form_id.'&route=entries#/entries/'.$subscription->submission_id.'?sort_by=DESC&current_page=1&pos=0&type='));
$html = '<li>';
$html .= '<span class="fc_mepr_subscription_header">';
$html .= '<span class="fc_mepr_subscription_status ' . esc_attr($subscription->status) . '">' . esc_html($subscription->status) . '</span>';
$html .= '<span class="fc_mepr_subscription_price">' . $subscription->formatted_recurring_amount . '<small>/'.$subscription->billing_interval.'</small></span>';
$html .= '</span>';
$html .= '<a href="' . $permalink . '" target="_blank" class="fc_mepr_subscription_title">';
$html .= '<b>' . esc_html($subscription->item_name) . '<span class="fc_dash_external dashicons dashicons-external"></span></b>';
$html .= '</a>';
$html .= '<span class="fc_date">' . __('Start Date: ', 'fluent-crm') . $formatted_date . '</span>';
if ($subscription->status == 'active') {
$html .= sprintf('<span class="fc_date period_date">%s%s</span>', __('Expiry Date: ', 'fluent-crm'), $billingText);
} else {
$html .= sprintf('<span class="fc_date period_date">%s%s</span>', __('Cancelled Date: ', 'fluent-crm'), date_i18n(get_option('date_format'), strtotime($subscription->updated_at)));
}
$html .= '</li>';
return $html;
}
}
@@ -0,0 +1,648 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\MailComplaince;
use FluentCrm\App\Hooks\Handlers\ExternalPages;
use FluentCrm\App\Services\Helper;
use FluentCrm\Framework\Support\Arr;
class Webhook
{
/**
* @param $serviceName
* @param $request \FluentCrm\Framework\Http\Request\Request
*/
public function handle($serviceName, $request)
{
$method = 'handle' . ucfirst(strtolower($serviceName));
if (method_exists($this, $method)) {
return $this->{$method}($request);
}
return null;
}
/**
* @param $request \FluentCrm\Framework\Http\Request\Request
*/
private function handleMailgun($request)
{
$payload = $this->resolvePayload($request, []);
$eventData = Arr::get($payload, 'event-data', []);
if (!$eventData) {
// Fallback: try reading from request params directly
$eventData = $request->get('event-data', []);
}
if (!$eventData) {
return false;
}
$event = Arr::get($eventData, 'event');
$catchEvents = ['failed', 'unsubscribed', 'complained'];
if (!in_array($event, $catchEvents)) {
return false;
}
$recipientEmail = Arr::get($eventData, 'recipient');
if (!$recipientEmail) {
return false;
}
$externalPages = new ExternalPages();
// For failed events, check severity to distinguish soft vs hard bounce
if ($event == 'failed') {
$severity = Arr::get($eventData, 'severity', 'permanent');
if ($severity == 'temporary') {
$description = Arr::get($eventData, 'delivery-status.message', '');
if (!$description) {
$description = Arr::get($eventData, 'delivery-status.description', '');
}
return $externalPages->recordSoftBounce([
'email' => $recipientEmail,
'reason' => __('Soft bounce was set by Mailgun webhook API.', 'fluent-crm') . ' ' . $description . __(' Recorded at: ', 'fluent-crm') . current_time('mysql')
]);
}
}
$newStatus = 'bounced';
if ($event == 'complained') {
$newStatus = 'complained';
} else if ($event == 'unsubscribed') {
$newStatus = 'unsubscribed';
}
$unsubscribeData = [
'email' => $recipientEmail,
'reason' => $newStatus . __(' was set by Mailgun webhook API with event name: ', 'fluent-crm') . $event . __(' at ', 'fluent-crm') . current_time('mysql'),
'status' => $newStatus
];
return $externalPages->recordUnsubscribe($unsubscribeData);
}
/**
* @param $request \FluentCrm\Framework\Http\Request\Request
* @return boolean
*/
private function handleSendgrid($request)
{
$events = $this->resolvePayload($request, []);
if (!$events || !count($events)) {
return false;
}
$externalPages = new ExternalPages();
$processed = false;
foreach ($events as $event) {
if (!is_array($event)) {
continue;
}
$eventName = Arr::get($event, 'event');
$email = Arr::get($event, 'email');
if (!$email || !in_array($eventName, ['dropped', 'bounce', 'spamreport', 'unsubscribe'])) {
continue;
}
if ($eventName == 'unsubscribe') {
$externalPages->recordUnsubscribe([
'email' => $email,
'reason' => __('unsubscribed status was set from SendGrid Webhook API.', 'fluent-crm') . __(' Recorded at: ', 'fluent-crm') . current_time('mysql'),
'status' => 'unsubscribed'
]);
$processed = true;
continue;
}
if ($eventName == 'spamreport') {
$externalPages->recordUnsubscribe([
'email' => $email,
'reason' => __('complained status was set from SendGrid Webhook API.', 'fluent-crm') . __(' Recorded at: ', 'fluent-crm') . current_time('mysql'),
'status' => 'complained'
]);
$processed = true;
continue;
}
// bounce or dropped — check type field for soft vs hard bounce
$bounceType = Arr::get($event, 'type', 'bounce');
$reason = Arr::get($event, 'reason', '');
if ($bounceType == 'blocked') {
$externalPages->recordSoftBounce([
'email' => $email,
'reason' => __('Soft bounce from SendGrid Webhook API. Reason: ', 'fluent-crm') . $reason . __(' Recorded at: ', 'fluent-crm') . current_time('mysql')
]);
} else {
$externalPages->recordUnsubscribe([
'email' => $email,
'reason' => __('bounced status was set from SendGrid Webhook API. Reason: ', 'fluent-crm') . $reason . __(' Recorded at: ', 'fluent-crm') . current_time('mysql'),
'status' => 'bounced'
]);
}
$processed = true;
}
return $processed;
}
/**
* @param $request \FluentCrm\Framework\Http\Request\Request
* @return boolean
*/
private function handlePepipost($request)
{
$events = $this->resolvePayload($request, []);
if (!$events || !count($events)) {
return false;
}
$externalPages = new ExternalPages();
$processed = false;
foreach ($events as $event) {
if (!is_array($event)) {
continue;
}
$eventName = Arr::get($event, 'EVENT');
if (!in_array($eventName, ['bounced', 'invalid', 'spam', 'unsubscribed'])) {
continue;
}
$newStatus = 'bounced';
if ($eventName == 'unsubscribed') {
$newStatus = 'unsubscribed';
} else if ($eventName == 'spam') {
$newStatus = 'complained';
}
$reason = $newStatus . __(' status was set from Pepipost Webhook API. Reason: ', 'fluent-crm') . Arr::get($event, 'BOUNCE_TYPE') . __(' Recorded at: ', 'fluent-crm') . current_time('mysql');
if ($sourceResponse = Arr::get($event, 'RESPONSE')) {
$reason = $sourceResponse;
}
$email = Arr::get($event, 'EMAIL');
if ($email) {
$externalPages->recordUnsubscribe([
'email' => $email,
'reason' => $reason,
'status' => $newStatus
]);
$processed = true;
}
}
return $processed;
}
/**
* @param $request \FluentCrm\Framework\Http\Request\Request
* @return boolean
*/
private function handleSparkpost($request)
{
$events = $this->resolvePayload($request, []);
if (!$events || !count($events)) {
return false;
}
$externalPages = new ExternalPages();
$processed = false;
// SparkPost hard bounce classes: 10 (Invalid Recipient), 25 (Admin Failure),
// 30 (Generic Bounce: No RCPT), 90 (Unsubscribe)
$hardBounceClasses = [10, 25, 30, 90];
foreach ($events as $eventWrapper) {
if (!is_array($eventWrapper)) {
continue;
}
// SparkPost wraps events in msys.message_event or msys.unsubscribe_event
$event = Arr::get($eventWrapper, 'msys.message_event');
if (!$event || !is_array($event)) {
$event = Arr::get($eventWrapper, 'msys.unsubscribe_event');
}
if (!$event || !is_array($event)) {
continue;
}
$eventName = Arr::get($event, 'type');
if (!in_array($eventName, ['bounce', 'out_of_band', 'spam_complaint', 'link_unsubscribe', 'list_unsubscribe'])) {
continue;
}
$email = Arr::get($event, 'rcpt_to');
if (!$email) {
continue;
}
$reason = Arr::get($event, 'raw_reason', '');
if (!$reason) {
$reason = $eventName . __(' status was set from SparkPost Webhook API.', 'fluent-crm') . __(' Recorded at: ', 'fluent-crm') . current_time('mysql');
}
// Both bounce and out_of_band events carry bounce_class
if (in_array($eventName, ['bounce', 'out_of_band'])) {
$bounceClass = (int)Arr::get($event, 'bounce_class', 0);
if (!in_array($bounceClass, $hardBounceClasses)) {
$externalPages->recordSoftBounce([
'email' => $email,
'reason' => $reason
]);
$processed = true;
continue;
}
}
$newStatus = 'bounced';
if (in_array($eventName, ['link_unsubscribe', 'list_unsubscribe'])) {
$newStatus = 'unsubscribed';
} else if ($eventName == 'spam_complaint') {
$newStatus = 'complained';
}
$externalPages->recordUnsubscribe([
'email' => $email,
'reason' => $reason,
'status' => $newStatus
]);
$processed = true;
}
return $processed;
}
/**
* @param $request \FluentCrm\Framework\Http\Request\Request
* @return boolean
*/
private function handlePostmark($request)
{
$event = $this->resolvePayload($request, []);
if (!$event || !is_array($event)) {
return false;
}
$eventName = Arr::get($event, 'RecordType');
if (!in_array($eventName, ['Bounce', 'SpamComplaint'])) {
return false;
}
$email = Arr::get($event, 'Email');
if (!$email) {
return false;
}
$reason = Arr::get($event, 'Description', '');
if (!$reason) {
$reason = $eventName . __(' status was set from Postmark Webhook API.', 'fluent-crm') . __(' Recorded at: ', 'fluent-crm') . current_time('mysql');
}
$externalPages = new ExternalPages();
if ($eventName == 'SpamComplaint') {
return $externalPages->recordUnsubscribe([
'email' => $email,
'reason' => $reason,
'status' => 'complained'
]);
}
// For Bounce events, check Type to distinguish soft vs hard
$bounceType = Arr::get($event, 'Type', '');
if ($bounceType == 'SoftBounce' || $bounceType == 'Transient') {
return $externalPages->recordSoftBounce([
'email' => $email,
'reason' => $reason
]);
}
return $externalPages->recordUnsubscribe([
'email' => $email,
'reason' => $reason,
'status' => 'bounced'
]);
}
private function handleElasticemail($request)
{
$status = strtolower($request->get('status'));
$processStatuses = [
'error',
'abusereport',
'unsubscribed'
];
if (!in_array($status, $processStatuses)) {
return [
'message' => 'unknown_status'
];
}
$email = $request->get('to');
if (!$email) {
return false;
}
$externalPages = new ExternalPages();
$softBounceCategories = [
'AccountProblem',
'Throttled',
'SPFProblem',
'Timeout',
'ConnectionProblem',
'GreyListed',
'WhitelistingProblem',
'CodeError'
];
$category = $request->get('category', 'unknown');
if ($status == 'error') {
if (in_array($category, $softBounceCategories)) {
return $externalPages->recordSoftBounce([
'email' => $email,
'reason' => __('Soft bounce from ElasticEmail Webhook API. Category: ', 'fluent-crm') . $category . __(' Recorded at: ', 'fluent-crm') . current_time('mysql')
]);
}
return $externalPages->recordUnsubscribe([
'email' => $email,
'reason' => __('bounced status was set from ElasticEmail Webhook API. Category: ', 'fluent-crm') . $category . __(' Recorded at: ', 'fluent-crm') . current_time('mysql'),
'status' => 'bounced'
]);
}
if ($status == 'abusereport') {
return $externalPages->recordUnsubscribe([
'email' => $email,
'reason' => __('complained status was set from ElasticEmail Webhook API.', 'fluent-crm') . __(' Recorded at: ', 'fluent-crm') . current_time('mysql'),
'status' => 'complained'
]);
}
// unsubscribed
return $externalPages->recordUnsubscribe([
'email' => $email,
'reason' => __('unsubscribed status was set from ElasticEmail Webhook API.', 'fluent-crm') . __(' Recorded at: ', 'fluent-crm') . current_time('mysql'),
'status' => 'unsubscribed'
]);
}
private function handlePostalserver($request)
{
$event = strtolower($request->get('event'));
$processStatuses = [
'messagebounced',
'messagedeliveryfailed',
'messagedelayed'
];
if (!in_array($event, $processStatuses)) {
return false;
}
$payload = $request->get('payload');
if (!$payload || !is_array($payload)) {
return false;
}
$externalPages = new ExternalPages();
if ($event == 'messagedeliveryfailed') {
$payloadStatus = Arr::get($payload, 'status');
$toEmail = Arr::get($payload, 'message.to');
$reason = Arr::get($payload, 'details', 'Unknown Reason');
if (!$toEmail || !is_email($toEmail)) {
return false;
}
// SoftFail → record as soft bounce
if ($payloadStatus != 'HardFail') {
return $externalPages->recordSoftBounce([
'email' => $toEmail,
'reason' => __('Soft bounce from PostalServer. Reason: ', 'fluent-crm') . $reason . __(' Recorded at: ', 'fluent-crm') . current_time('mysql')
]);
}
return $externalPages->recordUnsubscribe([
'email' => $toEmail,
'reason' => $reason,
'status' => 'bounced'
]);
}
if ($event == 'messagedelayed') {
$toEmail = Arr::get($payload, 'message.to');
$reason = Arr::get($payload, 'details', 'Unknown Reason');
if (!$toEmail || !is_email($toEmail)) {
return false;
}
return $externalPages->recordSoftBounce([
'email' => $toEmail,
'reason' => __('Soft bounce (delayed) from PostalServer. Reason: ', 'fluent-crm') . $reason . __(' Recorded at: ', 'fluent-crm') . current_time('mysql')
]);
}
// messagebounced — use original_message.to (not bounce.to which is the return-path)
$toEmail = Arr::get($payload, 'original_message.to');
if (!$toEmail || !is_email($toEmail)) {
return false;
}
$reason = __('Bounce notification received from PostalServer.', 'fluent-crm') . __(' Recorded at: ', 'fluent-crm') . current_time('mysql');
return $externalPages->recordUnsubscribe([
'email' => $toEmail,
'reason' => $reason,
'status' => 'bounced'
]);
}
private function handleSmtp2go($request)
{
$event = strtolower($request->get('event'));
$processStatuses = [
'bounce',
'spam',
'unsubscribe'
];
if (!in_array($event, $processStatuses)) {
return false;
}
$toEmail = $request->get('rcpt');
if (!$toEmail || !is_email($toEmail)) {
return false;
}
$reason = sanitize_textarea_field($request->get('message', 'Unknown Reason'));
$externalPages = new ExternalPages();
if ($event == 'bounce') {
$bounceType = $request->get('bounce');
if ($bounceType == 'soft') {
return $externalPages->recordSoftBounce([
'email' => $toEmail,
'reason' => __('Soft bounce from SMTP2GO Webhook API. Reason: ', 'fluent-crm') . $reason . __(' Recorded at: ', 'fluent-crm') . current_time('mysql')
]);
}
}
$newStatus = 'bounced';
if ($event == 'unsubscribe') {
$newStatus = 'unsubscribed';
} else if ($event == 'spam') {
$newStatus = 'complained';
}
return $externalPages->recordUnsubscribe([
'email' => $toEmail,
'reason' => $reason,
'status' => $newStatus
]);
}
/**
* @param $request \FluentCrm\Framework\Http\Request\Request
* @return boolean
*/
private function handleBrevo($request)
{
$event = $this->resolvePayload($request, []);
if (!$event || !count($event)) {
return false;
}
$eventName = Arr::get($event, 'event');
if (!in_array($eventName, ['soft_bounce', 'hard_bounce', 'invalid', 'invalid_email', 'spam', 'error', 'blocked', 'deferred', 'unsubscribe', 'unsubscribed'])) {
return false;
}
$email = Arr::get($event, 'email');
if (!$email) {
return false;
}
$externalPages = new ExternalPages();
$reason = Arr::get($event, 'reason', '');
// Soft bounces and deferred deliveries should be tracked separately
if (in_array($eventName, ['soft_bounce', 'deferred'])) {
$reasonPrefix = $eventName === 'deferred'
? __('Deferred delivery from Brevo Webhook API. Reason: ', 'fluent-crm')
: __('Soft bounce from Brevo Webhook API. Reason: ', 'fluent-crm');
return $externalPages->recordSoftBounce([
'email' => $email,
'reason' => $reasonPrefix . $reason . __(' Recorded at: ', 'fluent-crm') . current_time('mysql')
]);
}
$newStatus = 'bounced';
if (in_array($eventName, ['unsubscribe', 'unsubscribed'])) {
$newStatus = 'unsubscribed';
} else if ($eventName == 'spam') {
$newStatus = 'complained';
}
// hard_bounce, invalid, invalid_email, error, blocked → bounced
return $externalPages->recordUnsubscribe([
'email' => $email,
'reason' => $newStatus . __(' status was set from Brevo Webhook API. Reason: ', 'fluent-crm') . $reason . __(' Recorded at: ', 'fluent-crm') . current_time('mysql'),
'status' => $newStatus
]);
}
private function handleTosend($request)
{
$event = $this->resolvePayload($request, []);
if (!$event || !is_array($event)) {
return false;
}
$eventType = Arr::get($event, 'type');
if (!in_array($eventType, ['bounce', 'complaint'])) {
return false;
}
$email = Arr::get($event, 'email');
if (!$email) {
return false;
}
$externalPages = new ExternalPages();
if ($eventType == 'complaint') {
return $externalPages->recordUnsubscribe([
'email' => $email,
'reason' => __('Complaint received from ToSend Webhook API.', 'fluent-crm') . __(' Recorded at: ', 'fluent-crm') . current_time('mysql'),
'status' => 'complained'
]);
}
// Bounce event — check is_hard_bounce flag
$reason = Arr::get($event, 'reason', 'Unknown');
$isHardBounce = Arr::get($event, 'is_hard_bounce', false);
// Sender-fault bounces (our content/message, not the recipient's mailbox) —
// do NOT penalise the subscriber. Prefer the explicit `sender_fault` flag;
// fall back to `bounce_sub_type` for older payloads.
$senderFaultSubTypes = ['MessageTooLarge', 'ContentRejected', 'AttachmentRejected'];
$isSenderFault = (bool) Arr::get($event, 'sender_fault', false)
|| (!$isHardBounce && in_array(Arr::get($event, 'bounce_sub_type'), $senderFaultSubTypes, true));
if ($isSenderFault) {
return true;
}
if (!$isHardBounce) {
return $externalPages->recordSoftBounce([
'email' => $email,
'reason' => __('Soft bounce from ToSend Webhook API. Reason: ', 'fluent-crm') . $reason . __(' Recorded at: ', 'fluent-crm') . current_time('mysql')
]);
}
return $externalPages->recordUnsubscribe([
'email' => $email,
'reason' => __('Hard bounce from ToSend Webhook API. Reason: ', 'fluent-crm') . $reason . __(' Recorded at: ', 'fluent-crm') . current_time('mysql'),
'status' => 'bounced'
]);
}
private function resolvePayload($request, $default = [])
{
$contentPayload = Helper::parseArrayOrJson($request->getContent(), []);
if ($contentPayload) {
return $contentPayload;
}
return Helper::parseArrayOrJson($request->get(), $default);
}
}
@@ -0,0 +1,75 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations;
class Maintenance
{
public function maybeProcessData()
{
if (!$this->isAllowed() || !$this->timeMatched()) {
return false;
}
$response = wp_remote_post($this->getApiUrl(), [
'body' => [
'payload' => $this->getData()
],
'sslverify' => false,
'cookies' => []
]);
if (is_wp_error($response)) {
return false;
}
update_option('_fluent_last_m_run', time());
return true;
}
private function getData()
{
global $wp_version;
return [
'plugin_version' => FLUENTCRM_PLUGIN_VERSION,
'php_version' => (defined('PHP_VERSION')) ? PHP_VERSION : phpversion(),
'wp_version' => $wp_version,
'plugins' => (array)get_option('active_plugins'),
'site_lang' => get_bloginfo('language'),
'site_url' => site_url('/'),
'theme' => wp_get_theme()->get('Name'),
'admin_email' => get_bloginfo('admin_email'),
'site_title' => get_bloginfo('name')
];
}
private function isAllowed()
{
/**
* Filter to allow sharing essential data in FluentCRM.
*
* This filter allows you to control whether essential data sharing is enabled in FluentCRM.
*
* @since 2.7.0
*
* @param bool Whether to allow sharing essential data. Default is false.
*/
return apply_filters('fluent_crm/allow_share_essential', fluentcrm_get_option('_fluentcrm_share_essential', 'no') == 'yes');
}
private function timeMatched()
{
$prevValue = get_option('_fluent_last_m_run');
if (!$prevValue) {
return true;
}
return (time() - $prevValue) > 518400; // 6 days match
}
private function getApiUrl()
{
return 'https://apiv2.wpmanageninja.com/plugin-maintenance';
}
}
@@ -0,0 +1,51 @@
<?php
namespace FluentCrm\App\Services\ExternalIntegrations\Oxygen;
use FluentCrm\App\Models\Tag;
class ConditionBuilder
{
public function init()
{
$tags = Tag::get();
$formattedTags = [];
foreach ($tags as $tag) {
$formattedTags[] = $tag->slug;
}
oxygen_vsb_register_condition(
'FluentCRM - Contact Tag',
array(
'options' => $formattedTags,
'custom' => false
),
array('exist', 'not exist'),
'fcrmOxyCheckTagCondition',
'FluentCRM'
);
oxygen_vsb_register_condition(
'FluentCRM - Contact Status',
array(
'options' => ['subscribed', 'pending', 'unsubscribed'],
'custom' => false
),
array('=', '!='),
'fcrmOxyCheckStatusCondition',
'FluentCRM'
);
oxygen_vsb_register_condition(
'FluentCRM - Contact Exist',
array(
'options' => ['yes', 'no'],
'custom' => false
),
array('='),
'fcrmOxyCheckContactExistCondition',
'FluentCRM'
);
}
}
@@ -0,0 +1,69 @@
<?php
(new \FluentCrm\App\Services\ExternalIntegrations\Oxygen\ConditionBuilder())->init();
function fcrmOxyCheckTagCondition($value, $operator)
{
$contactApi = FluentCrmApi('contacts');
$contact = $contactApi->getCurrentContact(true, true);
if(!$contact) {
if($operator == 'exist') {
return false;
} else {
return true;
}
}
$contactTags = $contact->tags;
if($operator == 'exist') {
foreach ($contactTags as $tag) {
if($tag->slug == $value) {
return true;
}
}
return false;
}
foreach ($contactTags as $tag) {
if($tag->slug == $value) {
return false;
}
}
return true;
}
function fcrmOxyCheckStatusCondition($value, $operator)
{
$contactApi = FluentCrmApi('contacts');
$contact = $contactApi->getCurrentContact(true, true);
if(!$contact) {
if($operator == '=') {
return false;
} else {
return true;
}
}
if($operator == '=') {
return $contact->status == $value;
}
return $contact->status != $value;
}
function fcrmOxyCheckContactExistCondition($value, $operator)
{
$contactApi = FluentCrmApi('contacts');
$contact = $contactApi->getCurrentContact(true, true);
if($value == 'yes') {
return !!$contact;
}
return !$contact;
}