'ids' ) ) as $blog_id ) { switch_to_blog( $blog_id ); self::install(); restore_current_blog(); } return; } self::install(); } /** * Deactivation: unschedule background jobs only. Data is kept. */ public static function deactivate() { if ( function_exists( 'as_unschedule_all_actions' ) ) { as_unschedule_all_actions( 'loyaltypress_hourly', array(), 'loyaltypress' ); as_unschedule_all_actions( 'loyaltypress_daily', array(), 'loyaltypress' ); } } /** * Plugin deleted: remove data only if the admin opted in * under Settings → General → Uninstall. */ public static function uninstall() { if ( self::other_edition_installed() ) { return; // Free and Pro share data: keep it while another LoyaltySpin edition is still installed. } if ( is_multisite() ) { foreach ( get_sites( array( 'fields' => 'ids' ) ) as $blog_id ) { switch_to_blog( $blog_id ); self::uninstall_site(); restore_current_blog(); } return; } self::uninstall_site(); } /** * Whether more than one LoyaltySpin edition (free, Pro) is installed. During uninstall the * plugin being removed is still on disk, so two or more means another edition stays. * * @return bool */ private static function other_edition_installed() { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $found = 0; foreach ( array_keys( get_plugins() ) as $file ) { if ( preg_match( '#^(loyaltypress|loyaltyspin|loyaltypress-premium|loyaltypress-pro)/loyaltypress\.php$#', $file ) ) { ++$found; } } return $found > 1; } /** * Remove one site's data. */ private static function uninstall_site() { global $wpdb; $settings = get_option( 'loyaltypress_settings', array() ); if ( empty( $settings['general']['delete_data'] ) ) { return; } $tables = array( 'coin_ledger', 'spin_tokens', 'spin_prizes', 'spin_results', 'draws', 'draw_entries', 'draw_winners', 'draw_log', 'audit_log', 'rate_limits' ); foreach ( $tables as $t ) { $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $wpdb->prefix . 'loyaltypress_' . $t ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.SchemaChange } delete_option( 'loyaltypress_settings' ); delete_option( 'loyaltypress_db_version' ); delete_option( 'loyaltypress_flushed' ); $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE meta_key LIKE %s OR meta_key = %s', $wpdb->usermeta, $wpdb->esc_like( '_loyaltypress_' ) . '%', 'loyaltypress_birthday' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery if ( function_exists( 'as_unschedule_all_actions' ) ) { as_unschedule_all_actions( '', array(), 'loyaltypress' ); } } /** * Run upgrades if the stored DB version is older. */ public static function maybe_upgrade() { $stored = (string) get_option( 'loyaltypress_db_version' ); if ( LOYALTYPRESS_DB_VERSION !== $stored ) { if ( '' !== $stored && version_compare( $stored, '1.2.0', '<' ) ) { self::migrate_prefix(); } self::install(); } } /** * 1.2.0: tables and meta keys moved from the short "lp_" prefix to "loyaltypress_". */ private static function migrate_prefix() { global $wpdb; $tables = array( 'coin_ledger', 'spin_tokens', 'spin_prizes', 'spin_results', 'draws', 'draw_entries', 'draw_winners', 'draw_log', 'audit_log', 'rate_limits' ); // phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange foreach ( $tables as $t ) { $old = $wpdb->prefix . 'lp_' . $t; $new = $wpdb->prefix . 'loyaltypress_' . $t; if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $old ) ) ) && ! $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $new ) ) ) ) { $wpdb->query( $wpdb->prepare( 'RENAME TABLE %i TO %i', $old, $new ) ); } } $meta = array( $wpdb->postmeta, $wpdb->usermeta, $wpdb->prefix . 'woocommerce_order_itemmeta', $wpdb->prefix . 'wc_orders_meta' ); foreach ( $meta as $table ) { if ( ! $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ) ) { continue; } $wpdb->query( $wpdb->prepare( "UPDATE %i SET meta_key = CONCAT('_loyaltypress_', SUBSTRING(meta_key, 5)) WHERE meta_key LIKE %s", $table, $wpdb->esc_like( '_lp_' ) . '%' ) ); } $wpdb->query( $wpdb->prepare( "UPDATE %i SET meta_key = 'loyaltypress_birthday' WHERE meta_key = 'lp_birthday'", $wpdb->usermeta ) ); // phpcs:enable wp_cache_flush(); } /** * Create tables and default settings. */ public static function install() { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $c = $wpdb->get_charset_collate(); $p = $wpdb->prefix . 'loyaltypress_'; $sql = array(); // Append-only coin ledger. idem_key makes every award idempotent. $sql[] = "CREATE TABLE {$p}coin_ledger ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, order_id BIGINT UNSIGNED NOT NULL DEFAULT 0, type VARCHAR(20) NOT NULL, amount BIGINT NOT NULL, balance_after BIGINT NOT NULL, reason VARCHAR(255) NOT NULL DEFAULT '', idem_key VARCHAR(100) DEFAULT NULL, actor_id BIGINT UNSIGNED NOT NULL DEFAULT 0, expires_at DATETIME DEFAULT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY idem_key (idem_key), KEY user_id (user_id, id), KEY order_id (order_id), KEY expires (type, expires_at) ) $c;"; $sql[] = "CREATE TABLE {$p}spin_tokens ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, source_order_id BIGINT UNSIGNED NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'available', idem_key VARCHAR(100) DEFAULT NULL, created_at DATETIME NOT NULL, used_at DATETIME DEFAULT NULL, expires_at DATETIME DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY idem_key (idem_key), KEY user_status (user_id, status) ) $c;"; $sql[] = "CREATE TABLE {$p}spin_prizes ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, label VARCHAR(100) NOT NULL, type VARCHAR(30) NOT NULL, value DECIMAL(12,2) NOT NULL DEFAULT 0, weight INT UNSIGNED NOT NULL DEFAULT 1, stock INT DEFAULT NULL, color VARCHAR(9) NOT NULL DEFAULT '#4f46e5', image_id BIGINT UNSIGNED NOT NULL DEFAULT 0, sort_order INT NOT NULL DEFAULT 0, active TINYINT(1) NOT NULL DEFAULT 1, PRIMARY KEY (id) ) $c;"; $sql[] = "CREATE TABLE {$p}spin_results ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, token_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL, prize_id BIGINT UNSIGNED NOT NULL, prize_label VARCHAR(100) NOT NULL DEFAULT '', coupon_code VARCHAR(50) DEFAULT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY token_id (token_id), KEY user_id (user_id) ) $c;"; $sql[] = "CREATE TABLE {$p}draws ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(190) NOT NULL, description TEXT, trigger_type VARCHAR(20) NOT NULL, trigger_value DECIMAL(16,2) NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'open', config_json LONGTEXT, pre_commit_hash CHAR(64) DEFAULT NULL, snapshot_hash CHAR(64) DEFAULT NULL, entry_count INT UNSIGNED NOT NULL DEFAULT 0, starts_at DATETIME DEFAULT NULL, ends_at DATETIME DEFAULT NULL, drawn_at DATETIME DEFAULT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY status (status) ) $c;"; $sql[] = "CREATE TABLE {$p}draw_entries ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, draw_id BIGINT UNSIGNED NOT NULL, order_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, customer_key CHAR(32) NOT NULL DEFAULT '', entry_no INT UNSIGNED NOT NULL DEFAULT 1, order_total DECIMAL(16,2) NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'active', created_at DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY draw_order_entry (draw_id, order_id, entry_no), KEY draw_status (draw_id, status), KEY order_id (order_id) ) $c;"; $sql[] = "CREATE TABLE {$p}draw_winners ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, draw_id BIGINT UNSIGNED NOT NULL, entry_id BIGINT UNSIGNED NOT NULL, order_id BIGINT UNSIGNED NOT NULL DEFAULT 0, user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, win_rank INT UNSIGNED NOT NULL, prize_json TEXT, status VARCHAR(20) NOT NULL DEFAULT 'awarded', claimed_at DATETIME DEFAULT NULL, PRIMARY KEY (id), KEY draw_id (draw_id, win_rank), KEY user_id (user_id) ) $c;"; // Immutable log of draws (insert only, never updated). $sql[] = "CREATE TABLE {$p}draw_log ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, draw_id BIGINT UNSIGNED NOT NULL, event VARCHAR(40) NOT NULL, payload LONGTEXT, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY draw_id (draw_id) ) $c;"; $sql[] = "CREATE TABLE {$p}audit_log ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, actor_id BIGINT UNSIGNED NOT NULL DEFAULT 0, action VARCHAR(60) NOT NULL, object VARCHAR(190) NOT NULL DEFAULT '', details TEXT, ip_hash CHAR(64) DEFAULT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (id), KEY action (action) ) $c;"; $sql[] = "CREATE TABLE {$p}rate_limits ( rl_key VARCHAR(100) NOT NULL, hits INT UNSIGNED NOT NULL DEFAULT 0, window_start INT UNSIGNED NOT NULL, PRIMARY KEY (rl_key) ) $c;"; foreach ( $sql as $statement ) { dbDelta( $statement ); } if ( false === get_option( Settings::OPTION ) ) { add_option( Settings::OPTION, Settings::defaults(), '', false ); } self::seed_prizes(); update_option( 'loyaltypress_db_version', LOYALTYPRESS_DB_VERSION ); } /** * Default wheel slices so the wheel works out of the box. */ private static function seed_prizes() { global $wpdb; $table = $wpdb->prefix . 'loyaltypress_spin_prizes'; $count = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $table ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared if ( $count > 0 ) { return; } $defaults = array( array( '50 coins', 'coins', 50, 30, '#6366f1' ), array( '5% off', 'percent_coupon', 5, 20, '#0ea5e9' ), array( 'Better luck next time', 'none', 0, 25, '#94a3b8' ), array( '100 coins', 'coins', 100, 12, '#10b981' ), array( 'Free shipping', 'free_shipping', 0, 8, '#f59e0b' ), array( '10% off', 'percent_coupon', 10, 5, '#ef4444' ), ); foreach ( $defaults as $i => $d ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->insert( $table, array( 'label' => $d[0], 'type' => $d[1], 'value' => $d[2], 'weight' => $d[3], 'color' => $d[4], 'sort_order' => $i, 'active' => 1, ) ); } } }