<?php

/**
 * Plugin Name: Split Pay for Stripe Connect
 * Description:       Split payments made in WooCommerce stores between a Stripe Connected Account and a Stripe Platform Account
 * Version:           3.7.5
 * Requires at least: 5.2.3
 * Tested up to:      7.0
 * Requires PHP:      7.4
 * Author:            Gaucho Plugins
 * Author URI:        https://gauchoplugins.com
 * License:           GPL-3.0+
 * License URI:       http://www.gnu.org/licenses/gpl-3.0
 * Text Domain:       bsd-split-pay-stripe-connect-woo
 * Domain Path:       /languages
 *
 * WC requires at least: 3.7.1
 * WC tested up to: 10.7
 *
 * @package     bspscw
 */
namespace BSD_Split_Pay_Stripe_Connect_Woo;

// If this file is called directly, abort.
if ( !defined( 'WPINC' ) ) {
    die;
}
if ( !function_exists( 'add_action' ) ) {
    echo 'Hello! I\'m just a plugin. There\'s nothing I can do when called directly.';
    exit;
}
/*
 * Plugin constants.
 *
 * Hoisted out of the cold-load `else` branch below so PHPStan can statically
 * see the defines and other call sites referencing SPP_PLUGIN_URI / URL /
 * DIR / etc. don't trip the "constant not found" check (audit P1.2 — 203
 * findings). The `! defined()` guards are kept so a hot-load (plugin file
 * re-included for any reason) is a no-op.
 *
 * Runtime behavior is unchanged: in the cold load these run BEFORE the
 * Freemius init below (which used to also include them); in the hot load
 * they short-circuit on the guards. All callers see the same values they
 * always did.
 */
if ( !defined( 'SPP_PLUGIN_VER' ) ) {
    if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
        define( 'SPP_PLUGIN_VER', (string) time() );
    } else {
        define( 'SPP_PLUGIN_VER', '3.7.5' );
    }
}
if ( !defined( 'SPP_PLUGIN_MAIN_FILE_PATH' ) ) {
    define( 'SPP_PLUGIN_MAIN_FILE_PATH', __FILE__ );
}
/**
 * 3.7.5 (sc-7242b): WooCommerce HPOS + Cart/Checkout-Blocks compatibility — declared
 * here at file-scope (not inside a class method) so each plugin instance declares
 * for its own __FILE__.
 *
 * The legacy declaration inside `BSD_SCA::spp_hpos_declare_compatibility()` and
 * `WooCommerceIntegration::declareHposCompatibility()` both pass
 * SPP_PLUGIN_MAIN_FILE_PATH — but that constant is `defined()`-guarded, so on a
 * normal PRO+FREE co-install the constant resolves to whichever main file loaded
 * first (alphabetically PRO: `split-pay-plugin-pro/...`). FREE's class method
 * therefore declared compatibility against the wrong file path, leaving FREE
 * unflagged. WC's incompatibility tracker matches the declaration against the
 * basename of the active-plugin file, so FREE showed up as "Incompatible plugin
 * detected" on every PRO+FREE site even though SPP is fully HPOS-aware.
 *
 * Declaring here at file-scope uses the literal __FILE__ of THIS plugin file —
 * which is correct whether this is PRO or FREE — and bypasses the shared
 * constant entirely.
 */
add_action( 'before_woocommerce_init', static function () {
    if ( class_exists( '\\Automattic\\WooCommerce\\Utilities\\FeaturesUtil' ) ) {
        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true );
    }
} );
if ( !defined( 'SPP_PLUGIN_DIR' ) ) {
    define( 'SPP_PLUGIN_DIR', __DIR__ );
}
if ( !defined( 'SPP_PLUGIN_URL' ) ) {
    define( 'SPP_PLUGIN_URL', __FILE__ );
}
if ( !defined( 'SPP_PLUGIN_URI' ) ) {
    define( 'SPP_PLUGIN_URI', plugin_dir_url( __FILE__ ) );
}
if ( !defined( 'SPP_PLUGIN_ASSETS' ) ) {
    define( 'SPP_PLUGIN_ASSETS', plugin_dir_url( __FILE__ ) . 'assets' );
}
if ( !defined( 'SPP_PLUGIN_BASE_NAME' ) ) {
    define( 'SPP_PLUGIN_BASE_NAME', plugin_basename( __FILE__ ) );
}
if ( !defined( 'SPP_PLUGIN_INCLUDES' ) ) {
    define( 'SPP_PLUGIN_INCLUDES', SPP_PLUGIN_DIR . '/includes' );
}
if ( !defined( 'SPP_PLUGIN_UPGRADE_URL' ) ) {
    define( 'SPP_PLUGIN_UPGRADE_URL', '?billing_cycle=annual&page=bsd-split-pay-stripe-connect-woo-pricing' );
}
if ( !defined( 'SPP_STRP_ACCNT_TABLE' ) ) {
    define( 'SPP_STRP_ACCNT_TABLE', 'bsd_strp_accnt' );
}
if ( !defined( 'SPP_ALLOWED_HTML_TAGS' ) ) {
    define( 'SPP_ALLOWED_HTML_TAGS', array(
        'a' => array(
            'href'   => array(),
            'target' => array(),
        ),
    ) );
}
if ( !defined( 'SPP_SCA_PER_PAGE' ) ) {
    define( 'SPP_SCA_PER_PAGE', 100 );
}
/*
 * SPP_TEST_CONNECT_ID and SPP_LIVE_CONNECT_ID are intentionally identical
 * placeholders for the legacy Stripe Connect OAuth (client_id) flow which
 * has been superseded by the accountLinks-based onboarding implemented in
 * includes/admin/class-bsd-sca.php via $stripe->accountLinks->create().
 * Retained for backwards-compatibility with any third-party integration
 * that may still read them. Slated for removal in 3.8.0.
 *
 * @deprecated 3.7.0 Use the accountLinks onboarding flow.
 */
if ( !defined( 'SPP_TEST_CONNECT_ID' ) ) {
    define( 'SPP_TEST_CONNECT_ID', 'ca_NCjrUW8UGB4XZ2L6bRCi9vYD6pppYwlc' );
}
if ( !defined( 'SPP_LIVE_CONNECT_ID' ) ) {
    define( 'SPP_LIVE_CONNECT_ID', 'ca_NCjrUW8UGB4XZ2L6bRCi9vYD6pppYwlc' );
}
if ( !defined( 'SPP_TRANSFER_LOG_TABLE' ) ) {
    define( 'SPP_TRANSFER_LOG_TABLE', 'bsd_scsp_transfer_log' );
}
if ( !defined( 'SPP_CONNECTED_ID_TABLE' ) ) {
    define( 'SPP_CONNECTED_ID_TABLE', 'bsd_strp_connected_id' );
}
// Begin Freemius.
if ( function_exists( 'BSD_Split_Pay_Stripe_Connect_Woo\\bsdwcscsp_fs' ) ) {
    bsdwcscsp_fs()->set_basename( false, __FILE__ );
} else {
    // 3.7.5 static-analysis: outer if/else above already gates on
    // function_exists( ...\bsdwcscsp_fs ), so the duplicate inner
    // guard `if ( ! function_exists( ... ) )` was dead.
    /** Create a helper function for easy SDK access. */
    function bsdwcscsp_fs() {
        global $bsdwcscsp_fs;
        if ( !isset( $bsdwcscsp_fs ) ) {
            // Include Freemius SDK.
            require_once __DIR__ . '/freemius/start.php';
            $bsdwcscsp_fs = fs_dynamic_init( array(
                'id'               => '5491',
                'slug'             => 'bsd-split-pay-stripe-connect-woo',
                'premium_slug'     => 'split-pay-plugin-pro',
                'type'             => 'plugin',
                'public_key'       => 'pk_87287c4bbc0e070483f667959b2a1',
                'is_premium'       => false,
                'premium_suffix'   => 'PRO',
                'has_addons'       => false,
                'has_paid_plans'   => true,
                'menu'             => array(
                    'slug'       => 'bsd-split-pay-stripe-connect-woo-settings',
                    'first-path' => 'admin.php?page=bsd-split-pay-stripe-connect-woo-settings',
                    'support'    => false,
                ),
                'is_live'          => true,
                'is_org_compliant' => true,
            ) );
        }
        return $bsdwcscsp_fs;
    }

    // Init Freemius.
    bsdwcscsp_fs();
    // Signal that SDK was initiated.
    do_action( 'bsdwcscsp_fs_loaded' );
    // End Freemius.
    // SPP constants are now defined unconditionally at file scope above
    // (audit P1.2 — hoisted out of this else-branch so PHPStan can see
    // them). The `! defined()` guards make the hoist runtime-safe.
    function bsdwcscsp_fs_custom_icon() {
        return SPP_PLUGIN_DIR . '/assets/bsd-split-pay-stripe-connect-woo-icon-300x300.png';
    }

    /**
     * Guarded \WC_Stripe_Logger::log() shim — 3.7.4 (sc-7193).
     *
     * Mirrors the pattern in BSD_SCSP_Main_Plugin::safe_wcs_log() and
     * SplitPay\Integrations\WooCommerce\WooCommerceIntegration::safe_wcs_log(),
     * but exposed at namespace scope so admin / Frontend / non-class files
     * can call a single helper instead of duplicating the class_exists +
     * method_exists chain at every log site.
     *
     * Bare \WC_Stripe_Logger::log() calls cause a fatal on sites that run
     * Payment Plugins' Stripe gateway (woo-stripe-payment) WITHOUT the
     * official WooCommerce Stripe Payment Gateway. That class either does
     * not exist or implements log() as a removed-in-9.7 method depending
     * on the gateway combination. 3.7.1 + 3.7.3 fixed the bulk of the
     * call sites in the legacy main class + namespaced Core/ +
     * Integrations/ trees; this helper closes the remaining gap in
     * includes/admin/class-bsd-sca.php and any other admin/Frontend file
     * that needs to log without owning a class instance.
     *
     * @param string $message Log message.
     * @return void
     */
    if ( !function_exists( 'BSD_Split_Pay_Stripe_Connect_Woo\\spp_safe_wcs_log' ) ) {
        function spp_safe_wcs_log(  $message  ) {
            if ( class_exists( '\\WC_Stripe_Logger' ) ) {
                if ( method_exists( '\\WC_Stripe_Logger', 'log' ) ) {
                    \WC_Stripe_Logger::log( (string) $message );
                    return;
                }
                if ( method_exists( '\\WC_Stripe_Logger', 'debug' ) ) {
                    \WC_Stripe_Logger::debug( (string) $message );
                    return;
                }
            }
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log( (string) $message );
        }

    }
    /**
     * Per-order Stripe-idempotency attempt counter — 3.7.5 (sc-YA8).
     *
     * Returns `a1` on the first batch of transfers for an order, `a2` on the
     * first retry after a failure, `a3` on the next, etc. The value is appended
     * to every Stripe Transfer idempotency key SPP creates for that batch so
     * retries never collide with the prior attempt's cached request fingerprint.
     *
     * Without this suffix, Stripe rejects a retry whose params changed at all
     * (even by a single rounding cent) with `idempotency_key_in_use` — observed
     * in production on levinarf.com order 2166 / 2026-05-23: the destination
     * accounts lacked the `transfers` capability so the first attempt failed
     * 3×, the order then moved Processing → Completed, SPP retried, all 3
     * collided on the prior keys, and the real error was masked by the
     * idempotency error in the order notes.
     *
     * Both the legacy `class-bsd-split-pay-stripe-connect-woo.php::transfer_controller`
     * and `SplitPay\Core\TransferEngine::processTransfersLocked` call this once
     * at the top of every fresh batch. FluentCart's
     * `FluentCartIntegration::processTransfers` does the same. Each then
     * threads the returned salt to every Transfer::create idempotency_key.
     *
     * @param int $order_id WC / FluentCart order id.
     * @return string e.g. "a1", "a2". Always non-empty.
     */
    if ( !function_exists( 'BSD_Split_Pay_Stripe_Connect_Woo\\spp_bump_idem_attempt' ) ) {
        function spp_bump_idem_attempt(  $order_id  ) {
            $order_id = (int) $order_id;
            if ( $order_id <= 0 ) {
                return 'a1';
            }
            $current = 0;
            $order = ( function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : null );
            if ( $order && method_exists( $order, 'get_meta' ) ) {
                $current = (int) $order->get_meta( '_spp_idem_attempt', true );
            } elseif ( function_exists( 'get_post_meta' ) ) {
                $current = (int) get_post_meta( $order_id, '_spp_idem_attempt', true );
            }
            $next = $current + 1;
            try {
                if ( $order && method_exists( $order, 'update_meta_data' ) && method_exists( $order, 'save' ) ) {
                    $order->update_meta_data( '_spp_idem_attempt', (string) $next );
                    $order->save();
                } elseif ( function_exists( 'update_post_meta' ) ) {
                    update_post_meta( $order_id, '_spp_idem_attempt', (string) $next );
                }
            } catch ( \Throwable $e ) {
                // Non-fatal: if persistence fails we still proceed with the
                // computed salt. The worst case is the same salt being reused
                // on a future retry, which Stripe handles via its normal
                // idempotency cache (deduping rather than colliding).
                spp_safe_wcs_log( '[SPP] spp_bump_idem_attempt: could not persist for order ' . $order_id . ': ' . $e->getMessage() );
            }
            return 'a' . $next;
        }

    }
    // Load plugin icon from custom path.
    bsdwcscsp_fs()->add_filter( 'plugin_icon', 'BSD_Split_Pay_Stripe_Connect_Woo\\bsdwcscsp_fs_custom_icon' );
    /**
     * Plugin uninstall cleanup — Freemius `after_uninstall` hook.
     *
     * Freemius blocks plugins that ship a top-level `uninstall.php` because it
     * pre-empts FS's own uninstall lifecycle (the feedback survey, etc.).
     * Registering the cleanup against `after_uninstall` runs at the same
     * lifecycle moment (right after WordPress fires the uninstall hook) but
     * routes through FS so the feedback survey + telemetry still fire.
     *
     * Scope of deletion: every wp_option SPP writes during its lifetime + the
     * diagnostic cron hook. Sourced from plugin-testing-suite scope.yml +
     * 3.7.5-NEW additions (spp_deauthorized_accounts, spp_uninstall_remove_all_data).
     *
     * Destructive opt-in: when the customer has ticked the "Delete all plugin
     * data on uninstall" checkbox under Settings → Split Pay (the
     * `spp_uninstall_remove_all_data='1'` flag), this also drops the three
     * SPP-owned custom tables. Read the flag BEFORE the option sweep so it
     * survives long enough to be honoured here.
     *
     * Multisite (3.7.5 — sc-MULTISITE-UNINSTALL): WordPress fires the uninstall
     * hook ONCE — in the context of the single blog where the network admin
     * triggers deletion (the primary blog), NOT once per blog. So a naive
     * `delete_option()` + `$wpdb->prefix` DROP would purge only that one blog and
     * leave every OTHER subsite's options (including Stripe secret keys) and the
     * three per-blog custom tables as orphans — a data-minimisation breach
     * (PCI-DSS / GDPR) mirroring the per-blog activation loop in
     * includes/activate.php. We therefore iterate every blog under
     * `switch_to_blog()` and run the same per-blog cleanup in each, so options +
     * tables are removed network-wide. `delete_site_option()` (run once) covers
     * the network-wide variant some installs use for sitewide options.
     *
     * @since 3.7.5
     * @return void
     */
    function bsdwcscsp_fs_uninstall_cleanup() {
        // The canonical option-key inventory. Mirrors
        // plugin-testing-suite/plugins/spp/scope.yml::uninstall.removes_options
        // (22 entries) plus one 3.7.5-NEW entry. Any new wp_option SPP introduces
        // MUST be added here AND to the suite scope.yml at the same time.
        $spp_options_to_delete = array(
            // 3.7.4 / 3.7.5 migration sentinels — guard idempotent migrations.
            'bsd_split_pay_375_reconciled_stripe_mode',
            'bsd_split_pay_376_transfer_log_indexes',
            'bsd_split_pay_377_added_account_mode',
            'bsd_split_pay_377_modes_detected',
            'bsd_split_pay_374_recovery_notice',
            'bsd_split_pay_374_recovery_stats',
            // 3.7.5 — per-site mode-mismatch admin-notice state written by
            // includes/admin/spp-mode-mismatch-notice.php (SPP_MODE_MISMATCH_OPTION).
            // Dismissible via the persist-dismiss AJAX, but if never dismissed it
            // would survive uninstall — add it so removal is data-minimal.
            'bsd_spp_mode_mismatch_notices',
            // Plugin settings (set via Settings → Split Pay).
            'bsd_split_pay_stripe_connect_woo_back_end',
            'bsd_split_pay_stripe_connect_woo_exclude_shipping',
            'bsd_split_pay_stripe_connect_woo_exclude_tax',
            'bsd_split_pay_stripe_connect_woo_stripe_connected_account',
            'bsd_split_pay_stripe_connect_woo_transfer_percentage',
            // Schema-version sentinels written by includes/activate.php +
            // includes/admin/plugin-updates/class-db-updates.php.
            'bsd_log_table_created',
            'bsd_log_table_integration_source',
            'bsd_connected_id_table_created',
            'bsd_stripe_account_tbl_ver',
            'bsd_wc_scsp_db_version',
            'bsd_migration',
            // Misc SPP-owned wp_options (legacy + onboarding).
            'spp_account_countries',
            'spp_account_id_table',
            'spp_delayed_transfers_enabled',
            'spp_platform_country',
            'spp_setting_redirect',
            'spp_vendor_roles',
            'spp_keys_copied',
            // 3.7.5 NEW — AccountLifecycleHandler's deauthorized-accounts list.
            'spp_deauthorized_accounts',
            // 3.7.5 NEW — destructive-wipe opt-in flag. Always cleared so a
            // reinstall starts with the safe default ('0').
            'spp_uninstall_remove_all_data',
            // PT-fix-3.7.5 (security #259): Stripe secret-key options that
            // the source audit found weren't being cleared. Customer expects
            // secret keys wiped when plugin is uninstalled (PCI-DSS, GDPR
            // data minimisation). These are the 6 secret-key options +
            // 3 corresponding public-key options + the leftover misc options
            // from the pre-3.7.5 era. Added unconditionally (not just under
            // the destructive-wipe gate) because secrets-cleanup is the
            // minimum expected behavior on plugin removal.
            'stripe_test_api_secret_key',
            'stripe_test_api_public_key',
            'stripe_api_secret_key',
            'stripe_api_public_key',
            'spp_wc_stripe_test_secret_key',
            'spp_wc_stripe_live_secret_key',
            'spp_pp_stripe_test_secret_key',
            'spp_pp_stripe_live_secret_key',
            'spp_fc_stripe_test_secret_key',
            'spp_fc_stripe_live_secret_key',
            // Other SPP-owned options the source audit found unswept.
            'bsd_last_page',
            'sps_test_webhook_update',
            'sps_webhook_update',
            'last_fetched_account',
            'is_all_sca_fetched',
            'sending_meta',
            'transfer_taxes',
            'tax_transfer_type',
            'transfer_email',
            'coupon_adjust_transfers',
            'vendor_onboading',
            // note: documented misspelling, preserved for backcompat
            'enable_title_description',
            'onboarding_title',
            'onboarding_description',
            'bsd_trnsfr_shpng_fees',
        );
        // Per-blog cleanup. Runs in whatever blog context is current when called
        // (the loop below switches blogs on multisite). Reads the destructive-wipe
        // opt-in PER BLOG — it's a per-blog option, so each subsite honours its own
        // setting — and operates on that blog's `$wpdb->prefix` tables.
        $spp_purge_current_blog = static function () use($spp_options_to_delete) {
            // Destructive-wipe opt-in — captured BEFORE the option sweep so the
            // toggle survives long enough to be honoured here.
            $spp_should_drop_tables = get_option( 'spp_uninstall_remove_all_data', '0' ) === '1';
            foreach ( $spp_options_to_delete as $spp_option_key ) {
                delete_option( $spp_option_key );
            }
            // Conditional table drop — destructive, opt-in only.
            // Drop ONLY these three custom tables — never wp_postmeta, never
            // wp_usermeta, never wp_options (the sweep above handles that).
            // IF EXISTS keeps the call atomic on partially-bootstrapped installs.
            if ( $spp_should_drop_tables ) {
                global $wpdb;
                $spp_tables_to_drop = array($wpdb->prefix . 'bsd_scsp_transfer_log', $wpdb->prefix . 'bsd_strp_accnt', $wpdb->prefix . 'bsd_strp_connected_id');
                foreach ( $spp_tables_to_drop as $spp_table_name ) {
                    // $spp_table_name is built from $wpdb->prefix plus a hard-coded
                    // literal — no user input enters this string.
                    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery
                    $wpdb->query( "DROP TABLE IF EXISTS `{$spp_table_name}`" );
                }
            }
        };
        if ( is_multisite() ) {
            // WordPress fires uninstall ONCE (on the primary blog), so we must
            // iterate every blog ourselves or every OTHER subsite's options +
            // tables (incl. Stripe secret keys) leak as orphans. get_sites()
            // returns site objects; switch each, purge, then restore.
            $spp_blog_ids = ( function_exists( 'get_sites' ) ? get_sites( array(
                'fields' => 'ids',
                'number' => 0,
            ) ) : array() );
            foreach ( $spp_blog_ids as $spp_blog_id ) {
                switch_to_blog( (int) $spp_blog_id );
                $spp_purge_current_blog();
                restore_current_blog();
            }
            // Network-wide (sitewide) option variants some installs use. A
            // network option is single-instance, so clear these once, outside
            // the per-blog loop. delete_site_option() is a no-op when absent.
            foreach ( $spp_options_to_delete as $spp_option_key ) {
                delete_site_option( $spp_option_key );
            }
        } else {
            $spp_purge_current_blog();
        }
        // Scheduled actions / crons. SPP currently registers no production
        // crons on activation; the diagnostic-snapshot tool DOES schedule
        // `spp_diag_cleanup` when used, so clear it defensively here.
        wp_clear_scheduled_hook( 'spp_diag_cleanup' );
    }

    bsdwcscsp_fs()->add_action( 'after_uninstall', 'BSD_Split_Pay_Stripe_Connect_Woo\\bsdwcscsp_fs_uninstall_cleanup' );
    // Includes.
    include 'includes/init.php';
    include 'includes/admin/settings-api.php';
    include 'includes/admin/init.php';
    include 'includes/activate.php';
    include 'includes/class-bsd-split-pay-stripe-connect-woo.php';
    include 'includes/admin/menus.php';
    include 'includes/admin/options-page.php';
    include 'includes/admin/class-bsd-sca.php';
    include 'includes/admin/spp-validation-notices.php';
    include 'includes/admin/spp-374-recovery-notice.php';
    include 'includes/admin/spp-per-product-recovery.php';
    include 'includes/admin/spp-mode-mismatch-notice.php';
    require_once __DIR__ . '/includes/autoloader.php';
    // 3.7.5 payments-grade CLI tooling (sc-PAYMENTS-GRADE-CLI 2026-05-24):
    // `wp spp verify-integrity`, `wp spp dump-transfer-log`, `wp spp retry-failed-transfers`.
    // File self-guards on `defined('WP_CLI') && WP_CLI`, so loading it
    // unconditionally is safe — no-op outside the WP-CLI runtime.
    if ( defined( 'WP_CLI' ) && WP_CLI ) {
        require_once __DIR__ . '/includes/Cli/SppCommands.php';
    }
    // Hooks.
    register_activation_hook( __FILE__, 'BSD_Split_Pay_Stripe_Connect_Woo\\Inc\\bsd_scsp_activate_plugin' );
    add_action( 'init', 'BSD_Split_Pay_Stripe_Connect_Woo\\Inc\\bsd_scsp_db_update' );
    add_action( 'admin_init', 'BSD_Split_Pay_Stripe_Connect_Woo\\Inc\\Admin\\bsd_scsp_admin_init' );
    add_action( 'admin_menu', 'BSD_Split_Pay_Stripe_Connect_Woo\\Inc\\Admin\\bsd_scsp_admin_menus' );
    // Consider moving the following functions to a separate class.
    /**
     * Check for the existence of WooCommerce and any other requirements.
     */
    if ( !function_exists( 'BSD_Split_Pay_Stripe_Connect_Woo\\bsd_scsp_check_requirements' ) ) {
        function bsd_scsp_check_requirements() {
            if ( !function_exists( 'is_plugin_active' ) ) {
                require_once ABSPATH . '/wp-admin/includes/plugin.php';
            }
            $wc_active = is_plugin_active( 'woocommerce/woocommerce.php' );
            $wc_stripe_active = is_plugin_active( 'woocommerce-gateway-stripe/woocommerce-gateway-stripe.php' );
            $wc_stripe_pp_active = is_plugin_active( 'woo-stripe-payment/woo-stripe-payment.php' );
            $fc_active = is_plugin_active( 'fluent-cart/fluent-cart.php' );
            $has_wc_integration = $wc_active && ($wc_stripe_active || $wc_stripe_pp_active);
            $has_fc_integration = $fc_active;
            if ( !$has_wc_integration && !$has_fc_integration ) {
                add_action( 'admin_notices', 'BSD_Split_Pay_Stripe_Connect_Woo\\bsd_scsp_missing_wc_notice' );
                return false;
            }
            return true;
        }

    }
    global $bsd_sca, $bsd_stripe_account_tbl_ver;
    $bsd_stripe_account_tbl_ver = '1.0';
    /**
     * Begins execution of the plugin.
     */
    if ( !function_exists( 'BSD_Split_Pay_Stripe_Connect_Woo\\run_BSD_Split_Pay_Stripe_Connect_Woo' ) ) {
        function run_BSD_Split_Pay_Stripe_Connect_Woo() {
            if ( bsd_scsp_check_requirements() ) {
                global $bsd_sca;
                $plugin = new Inc\BSD_SCSP_Main_Plugin();
                $plugin->run();
                $bsd_sca = new Inc\Admin\BSD_SCA();
                $bsd_sca->run();
            }
        }

    }
    run_BSD_Split_Pay_Stripe_Connect_Woo();
    // 3.7.5 (sc-7195): co-installed-plugin detector. Loaded unconditionally —
    // the detector file only defines functions and binds to admin_notices /
    // network_admin_notices, both of which only fire in admin context anyway.
    // (The previous is_admin() gate meant the functions weren't defined in
    // wp-cli contexts, which made the detector hard to test programmatically
    // and would also silently fail if something fired admin_notices outside
    // the normal admin-page lifecycle.) The notice handler itself gates on
    // current_user_can('activate_plugins'), so non-admins still see nothing.
    $_spp_coinstall = __DIR__ . '/includes/admin/co-installed-detector.php';
    if ( is_readable( $_spp_coinstall ) ) {
        require_once $_spp_coinstall;
    }
    // 3.7.5: WCFM Multivendor compatibility shim. Detects WCFM and installs
    // a shutdown-action fallback that re-emits the SPP save action when WCFM's
    // intercepting save pipeline swallowed woocommerce_process_product_meta.
    // Registered at plugins_loaded:5 — BEFORE the integration registry binds
    // anything at :30 — so the markSaved listener is in place before any
    // save handler from SPP or anyone else runs. No-op on non-WCFM sites.
    add_action( 'plugins_loaded', function () {
        \SplitPay\Integrations\WooCommerce\WCFMCompat::register();
    }, 5 );
    add_action( 'plugins_loaded', function () {
        $registry = \SplitPay\Core\IntegrationRegistry::getInstance();
        $registry->register( new \SplitPay\Integrations\WooCommerce\WooCommerceIntegration() );
        $registry->register( new \SplitPay\Integrations\FluentCart\FluentCartIntegration() );
        /**
         * Allow third-party plugins to register additional integrations on the
         * shared IntegrationRegistry. Fires after the bundled WooCommerce and
         * FluentCart integrations are registered but before each integration's
         * hooks are bound.
         *
         * @since 3.7.0
         *
         * @param \SplitPay\Core\IntegrationRegistry $registry Shared registry.
         */
        do_action( 'spp_register_integrations', $registry );
        /**
         * Allow third-party code to filter the registry (e.g. to swap or remove
         * an integration) before hooks are bound.
         *
         * @since 3.7.0
         *
         * @param \SplitPay\Core\IntegrationRegistry $registry Shared registry.
         */
        $registry = apply_filters( 'spp_integrations', $registry );
        foreach ( $registry->getActive() as $integration ) {
            $integration->registerHooks();
            if ( is_admin() ) {
                $integration->registerAdminUI();
            }
        }
        ( new \SplitPay\Frontend\VendorConnectShortcode() )->register();
    }, 30 );
    /**
     * Display a message advising WooCommerce is required.
     */
    if ( !function_exists( 'BSD_Split_Pay_Stripe_Connect_Woo\\bsd_scsp_missing_wc_notice' ) ) {
        function bsd_scsp_missing_wc_notice() {
            $class = 'notice notice-error';
            $message = __( 'The Split Pay Plugin requires either WooCommerce with a Stripe gateway plugin, or FluentCart with Stripe configured.', 'bsd-split-pay-stripe-connect-woo' );
            printf( '<div class="%1$s"><p>%2$s</p></div>', esc_attr( $class ), esc_html( $message ) );
        }

    }
}