prefix . 'storesuit_stock_alerts'; } public static function install(): void { // 2: the cancel key is indexed (Denetim R0-8): the public cancel route looked it up among every waiting request. if ( get_option( self::SCHEMA ) === '2' ) { return; } global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $table = self::table(); $charset = $wpdb->get_charset_collate(); dbDelta( "CREATE TABLE $table ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, product_id bigint(20) unsigned NOT NULL, email varchar(190) NOT NULL, email_hash char(64) NOT NULL, customer_id bigint(20) unsigned NOT NULL DEFAULT 0, status varchar(12) NOT NULL DEFAULT 'waiting', token char(32) NOT NULL, created datetime NOT NULL, notified datetime NULL, PRIMARY KEY (id), UNIQUE KEY product_email (product_id,email_hash), KEY status_product (status,product_id), KEY email_hash (email_hash), KEY token (token) ) $charset;" ); update_option( self::SCHEMA, '2', false ); } public static function hooks(): void { add_filter( 'woocommerce_get_stock_html', array( __CLASS__, 'form' ), 20, 2 ); add_action( 'wp_enqueue_scripts', array( __CLASS__, 'assets' ) ); add_action( 'rest_api_init', array( __CLASS__, 'routes' ) ); add_action( 'woocommerce_product_set_stock_status', array( __CLASS__, 'restocked' ), 10, 2 ); add_action( 'woocommerce_variation_set_stock_status', array( __CLASS__, 'restocked' ), 10, 2 ); add_action( 'transition_post_status', array( __CLASS__, 'republished' ), 10, 3 ); add_action( self::HOOK, array( __CLASS__, 'send_batch' ) ); add_action( self::CLEANUP, array( __CLASS__, 'cleanup' ) ); add_filter( 'woocommerce_email_classes', static function ( $emails ) { require_once __DIR__ . '/StockAlertEmail.php'; $emails['StoreSuit_Stock_Alert'] = new StockAlertEmail(); return $emails; } ); add_filter( 'wp_privacy_personal_data_exporters', static function ( $exporters ) { $exporters['storesuit-stock-alerts'] = array( 'exporter_friendly_name'=>I18n::t( 'Back-in-stock requests' ), 'callback'=>array( __CLASS__, 'export' ) ); return $exporters; } ); add_filter( 'wp_privacy_personal_data_erasers', static function ( $erasers ) { $erasers['storesuit-stock-alerts'] = array( 'eraser_friendly_name'=>I18n::t( 'Back-in-stock requests' ), 'callback'=>array( __CLASS__, 'erase' ) ); return $erasers; } ); add_action( 'init', static function () { if ( ! wp_next_scheduled( self::CLEANUP ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::CLEANUP ); } }, 99 ); } /* ---------- Settings ---------- */ public static function settings(): array { $saved = get_option( self::SETTINGS, array() ); $saved = is_array( $saved ) ? $saved : array(); return array( 'enabled'=>! empty( $saved['enabled'] ), 'auto_send'=>! array_key_exists( 'auto_send', $saved ) || ! empty( $saved['auto_send'] ) ); } /* ---------- Storefront ---------- */ /** On sale in the store: published, and for a variation its product too (a variation keeps "publish" when its product goes back to draft). */ private static function published( \WC_Product $product ): bool { return $product->get_status() === 'publish' && ( ! $product->is_type( 'variation' ) || get_post_status( $product->get_parent_id() ) === 'publish' ); } private static function eligible( $product ): bool { // A variation of a product taken off sale takes no new request (Codex SA-02). return $product instanceof \WC_Product && ! $product->is_type( array( 'variable', 'grouped', 'external' ) ) && ! $product->is_in_stock() && self::published( $product ); } /** * Not a