diff --git a/2meet-data-optimizer-woocommerce-addon.php b/2meet-data-optimizer-woocommerce-addon.php index c8c1747..18f80fd 100644 --- a/2meet-data-optimizer-woocommerce-addon.php +++ b/2meet-data-optimizer-woocommerce-addon.php @@ -3,7 +3,7 @@ * Plugin Name: 2meet Data Optimizer — Woocommerce AddOn * Plugin URI: https://2meet.io/2meet-data-optimizer/woocommerce * Description: WooCommerce 整合:20 表 schema 註冊 + commission tracking (wp_wpdo_wc_commissions) + product/order zone + HPOS notice - * Version: 0.1.1 + * Version: 0.2.0 * Requires at least: 6.0 * Tested up to: 6.9.4 * Requires PHP: 8.1 @@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) { exit; } -define( 'TMDO_WOOCOMMERCE_VERSION', '0.1.1' ); +define( 'TMDO_WOOCOMMERCE_VERSION', '0.2.0' ); define( 'TMDO_WOOCOMMERCE_PATH', plugin_dir_path( __FILE__ ) ); define( 'TMDO_WOOCOMMERCE_URL', plugin_dir_url( __FILE__ ) ); define( 'TMDO_WOOCOMMERCE_FILE', __FILE__ ); diff --git a/CHANGELOG.md b/CHANGELOG.md index 172d20c..cdb8fa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,36 @@ Versioning follows [Semantic Versioning](https://semver.org/). --- +## [0.2.0] — 2026-09-27 + +Released with 2meet-data-optimizer 1.2.0 (hardening 2.2, search-index mode +decision 2: WooCommerce meta stays native). + +### Removed + +- WooCommerce meta is no longer copied to flat tables. WooCommerce reads its + meta directly and writes stock / sales by SQL, so a flat copy drifted + (R-2.1: stock and `total_sales` out of step after an order). No longer + registered: + - product and order zone fields (`wpdo_register_fields`), including the + subscription fields; + - the `user` / `hot` entity group (`wpdo_register_entity_fields`), + including the three fields added in 0.1.1. +- Kept: the `wp_wc_*` custom table registry, the HPOS notice, the + Marketplace commission tracking and the term count filter. + +### Upgrading + +- Nothing is dropped. With the user entity below `aeav_only`, WooCommerce keeps + reading native user meta and the `wp_wpdo_user_hot` table is simply no + longer written. In `aeav_only`, core restores the group as an orphan so its + values stay readable; `wp tmdo bridge-restore-native user --group=hot --retire` + writes them back to native and archives the table. +- Product flat data (core's former `post/wc_product` group): see + 2meet-data-optimizer 1.2.0. + +--- + ## [0.1.1] — 2026-09-26 Hardening 2.0 changes, released alongside 2meet-data-optimizer 1.1.0. diff --git a/includes/class-tmdo-woocommerce.php b/includes/class-tmdo-woocommerce.php index 60df4df..bdb8fdd 100644 --- a/includes/class-tmdo-woocommerce.php +++ b/includes/class-tmdo-woocommerce.php @@ -6,10 +6,9 @@ * `wp_wc_product_meta_lookup` for product search). WPDO's job is to: * 1. Register the 20 `wp_wc_*` tables to Custom_Table_Registry * (so doctor / conflict-scan know about them) - * 2. Add hot-zone fields to Schema_Registry for the legacy postmeta keys - * WC still uses on products / orders (when HPOS off) - * 3. Detect HPOS state and surface it to admin - * 4. NOT duplicate what WC already does (no shadow lookup tables) + * 2. Detect HPOS state and surface it to admin + * 3. NOT duplicate what WC already does (no shadow lookup tables, and since + * 0.2.0 no flat copy of product / order / customer meta) * * This is "Tier 5" in ENTITY_ADAPTER_COOKBOOK.md: integrating with a plugin * that has its own anti-EAV solution. @@ -171,11 +170,9 @@ final class TMDO_WooCommerce { } add_action( 'wpdo_register_custom_tables', array( __CLASS__, 'register_custom_tables' ) ); - add_action( 'wpdo_register_fields', array( __CLASS__, 'register_schema_fields' ) ); - // v2.1.6: customer hot fields (_money_spent / _order_count / _last_order) move - // to Entity_Registry so Hook Bus can route them to wp_wpdo_user_hot once - // `entity_user` mode advances past `disabled`. See PLAN-entity-user-cutover.md. - add_action( 'wpdo_register_entity_fields', array( __CLASS__, 'register_user_entity_fields' ) ); + // 0.2.0: no product / order / subscription zone fields and no user hot group. + // WooCommerce reads its meta directly and writes stock by SQL, so a flat + // copy drifts; its meta stays native only (core 1.2.0 dropped wc_product too). // HPOS recommendation notice — only on WC admin pages, only when: // (a) HPOS is OFF, (b) the threshold of legacy orders has been crossed. @@ -339,328 +336,6 @@ final class TMDO_WooCommerce { ); } - /** - * Register hot-zone meta fields for products + orders + customers. - * - * Delegates to dedicated registrars to keep this method readable. - * - * @param mixed $schema TMDO_Schema_Registry (passed by action). - * @return void - */ - public static function register_schema_fields( $schema = null ): void { - if ( ! class_exists( 'TMDO_Schema_Registry' ) ) { - return; - } - $schema = $schema ?: TMDO_Schema_Registry::instance(); - - self::register_product_fields( $schema ); - - // Order fields only matter when HPOS is OFF (otherwise WC stores them in wc_orders_meta). - if ( ! self::is_hpos_enabled() ) { - self::register_order_fields( $schema ); - } - - // v2.1.6: Customer usermeta hot fields moved out of Schema_Registry — they - // are now registered via `register_user_entity_fields()` on the - // `wpdo_register_entity_fields` action so Hook Bus can intercept them. - // Schema_Registry never had a user-meta interceptor; the previous - // registration was inert. See PLAN-entity-user-cutover.md. - - // Subscription fields if WC Subscriptions OR wc-linepay-subscription is active. - if ( self::has_subscriptions() ) { - self::register_subscription_fields( $schema ); - } - } - - /** - * Product hot fields (search/filter shop pages). - * - * @param TMDO_Schema_Registry $schema Registry. - * @return void - */ - private static function register_product_fields( $schema ): void { - $schema->register_many( - self::PROVIDER, - array( - array( - 'post_type' => 'product', - 'meta_key' => '_price', - 'zone' => 'hot', - 'data_type' => 'decimal(15,4) NOT NULL DEFAULT 0', - 'column' => '_price', - 'indexed' => true, - ), - array( - 'post_type' => 'product', - 'meta_key' => '_regular_price', - 'zone' => 'hot', - 'data_type' => 'decimal(15,4) NOT NULL DEFAULT 0', - 'column' => '_regular_price', - ), - array( - 'post_type' => 'product', - 'meta_key' => '_sale_price', - 'zone' => 'hot', - 'data_type' => 'decimal(15,4) NULL', - 'column' => '_sale_price', - ), - array( - 'post_type' => 'product', - 'meta_key' => '_sku', - 'zone' => 'hot', - 'data_type' => "varchar(100) NOT NULL DEFAULT ''", - 'column' => '_sku', - 'indexed' => true, - ), - array( - 'post_type' => 'product', - 'meta_key' => '_stock', - 'zone' => 'hot', - 'data_type' => 'decimal(15,4) NULL', - 'column' => '_stock', - ), - array( - 'post_type' => 'product', - 'meta_key' => '_stock_status', - 'zone' => 'hot', - 'data_type' => "varchar(20) NOT NULL DEFAULT 'instock'", - 'column' => '_stock_status', - 'indexed' => true, - ), - array( - 'post_type' => 'product', - 'meta_key' => '_visibility', - 'zone' => 'hot', - 'data_type' => "varchar(20) NOT NULL DEFAULT 'visible'", - 'column' => '_visibility', - ), - array( - 'post_type' => 'product', - 'meta_key' => '_featured', - 'zone' => 'hot', - 'data_type' => "varchar(3) NOT NULL DEFAULT 'no'", - 'column' => '_featured', - 'indexed' => true, - ), - array( - 'post_type' => 'product', - 'meta_key' => '_tax_status', - 'zone' => 'hot', - 'data_type' => "varchar(20) NOT NULL DEFAULT 'taxable'", - 'column' => '_tax_status', - ), - array( - 'post_type' => 'product', - 'meta_key' => '_virtual', - 'zone' => 'hot', - 'data_type' => "varchar(3) NOT NULL DEFAULT 'no'", - 'column' => '_virtual', - ), - array( - 'post_type' => 'product', - 'meta_key' => '_downloadable', - 'zone' => 'hot', - 'data_type' => "varchar(3) NOT NULL DEFAULT 'no'", - 'column' => '_downloadable', - ), - ) - ); - - // Cold zone — large display fields rarely searched but always shown. - $schema->register_many( - self::PROVIDER, - array( - array( - 'post_type' => 'product', - 'meta_key' => '_product_attributes', - 'zone' => 'cold', - 'cache_group' => 'wpdo_cold_product', - 'cache_ttl' => HOUR_IN_SECONDS, - ), - array( - 'post_type' => 'product', - 'meta_key' => '_purchase_note', - 'zone' => 'cold', - 'cache_group' => 'wpdo_cold_product', - 'cache_ttl' => HOUR_IN_SECONDS, - ), - ) - ); - } - - /** - * Order hot fields (legacy postmeta path; only registered when HPOS off). - * - * @param TMDO_Schema_Registry $schema Registry. - * @return void - */ - private static function register_order_fields( $schema ): void { - $schema->register_many( - self::PROVIDER, - array( - array( - 'post_type' => 'shop_order', - 'meta_key' => '_order_total', - 'zone' => 'hot', - 'data_type' => 'decimal(15,4) NOT NULL DEFAULT 0', - 'column' => '_order_total', - 'indexed' => true, - ), - array( - 'post_type' => 'shop_order', - 'meta_key' => '_order_currency', - 'zone' => 'hot', - 'data_type' => "varchar(8) NOT NULL DEFAULT ''", - 'column' => '_order_currency', - ), - array( - 'post_type' => 'shop_order', - 'meta_key' => '_billing_email', - 'zone' => 'hot', - 'data_type' => "varchar(254) NOT NULL DEFAULT ''", - 'column' => '_billing_email', - 'indexed' => true, - ), - array( - 'post_type' => 'shop_order', - 'meta_key' => '_payment_method', - 'zone' => 'hot', - 'data_type' => "varchar(60) NOT NULL DEFAULT ''", - 'column' => '_payment_method', - 'indexed' => true, - ), - array( - 'post_type' => 'shop_order', - 'meta_key' => '_customer_user', - 'zone' => 'hot', - 'data_type' => 'bigint(20) NOT NULL DEFAULT 0', - 'column' => '_customer_user', - 'indexed' => true, - ), - ) - ); - } - - /** - * Customer usermeta hot fields — registered to Entity_Registry so Hook Bus can - * route them to `wp_wpdo_user_hot` once `entity_user` mode advances past `disabled`. - * - * Replaces the v2.1.5 `register_customer_fields()` which targeted Schema_Registry - * (zone-based, no user interceptor) — that path was inert. See - * PLAN-entity-user-cutover.md for the migration playbook. - * - * Type mapping (Entity_Registry uses fixed-precision DECIMAL(18,6) / BIGINT(20), - * which is a superset of the prior `decimal(15,4)` / `bigint(20)` declarations — - * no precision loss): - * - _money_spent → decimal (DECIMAL(18,6)) - * - _order_count → integer (BIGINT(20)) - * - _last_order → integer (BIGINT(20)) - * - * @return void - */ - public static function register_user_entity_fields(): void { - if ( ! class_exists( 'TMDO_Entity_Registry' ) ) { - return; - } - TMDO_Entity_Registry::register_group( - 'user', - 'hot', - array( - array( - 'key' => '_money_spent', - 'type' => 'decimal', - 'searchable' => true, - 'label' => 'WC customer cumulative spend', - ), - array( - 'key' => '_order_count', - 'type' => 'integer', - 'searchable' => true, - 'label' => 'WC customer order count', - ), - array( - 'key' => '_last_order', - 'type' => 'integer', - 'label' => 'WC customer last order ID', - ), - // WC runtime stats — written on order/session events, high-frequency - // on busy stores. Unregistered they land back in wp_usermeta. - array( - 'key' => 'wc_last_active', - 'type' => 'integer', - 'label' => 'WC last customer activity timestamp (Unix)', - ), - array( - // WooCommerce stores this per-site via Users::update_site_user_meta(), - // which appends the de-suffixed blog prefix ('wc_order_count' + '_wp' - // on the main site, '_wp_2' on a subsite). Compute it the same way - // rather than hardcoding the main-site form. - 'key' => 'wc_order_count_' . rtrim( $GLOBALS['wpdb']->get_blog_prefix(), '_' ), - 'type' => 'integer', - 'label' => 'WC order count for the current site (blog-aware key)', - ), - array( - 'key' => 'last_update', - 'type' => 'integer', - 'label' => 'WC customer last profile update timestamp (Unix)', - ), - ) - ); - } - - /** - * Subscription product fields (wc-subscriptions or wc-linepay-subscription). - * - * @param TMDO_Schema_Registry $schema Registry. - * @return void - */ - private static function register_subscription_fields( $schema ): void { - $schema->register_many( - self::PROVIDER, - array( - array( - 'post_type' => 'product', - 'meta_key' => '_subscription_price', - 'zone' => 'hot', - 'data_type' => 'decimal(15,4) NULL', - 'column' => '_subscription_price', - ), - array( - 'post_type' => 'product', - 'meta_key' => '_subscription_period', - 'zone' => 'hot', - 'data_type' => 'varchar(20) NULL', - 'column' => '_subscription_period', - ), - array( - 'post_type' => 'product', - 'meta_key' => '_subscription_period_interval', - 'zone' => 'hot', - 'data_type' => 'int(11) NULL', - 'column' => '_subscription_period_interval', - ), - array( - 'post_type' => 'product', - 'meta_key' => '_subscription_length', - 'zone' => 'hot', - 'data_type' => 'int(11) NULL', - 'column' => '_subscription_length', - ), - ) - ); - } - - /** - * Subscription extension detection — WC native or wc-linepay-subscription. - * - * @return bool - */ - private static function has_subscriptions(): bool { - return class_exists( 'WC_Subscriptions' ) - || defined( 'WCS_INIT_TIMESTAMP' ) - || file_exists( WP_PLUGIN_DIR . '/wc-linepay-subscription/wc-linepay-subscription.php' ); - } - /** * WPDO doctor probe for any wc_* table — verifies existence + reports row count. * diff --git a/tests/unit/WooCommerceIntegrationTest.php b/tests/unit/WooCommerceIntegrationTest.php index 1d1120b..636cc2d 100644 --- a/tests/unit/WooCommerceIntegrationTest.php +++ b/tests/unit/WooCommerceIntegrationTest.php @@ -230,4 +230,22 @@ class WooCommerceIntegrationTest extends TestCase { $pp->setValue( $registry, $original ); } } + + // ── 2.2 PR-085: WooCommerce data stays in native meta ─────────────────── + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_register_keeps_woocommerce_meta_out_of_the_flat_tables(): void { + eval( 'class WooCommerce {}' ); // phpcs:ignore Squiz.PHP.Eval -- WooCommerce stand-in. + $GLOBALS['_wp_filter_callbacks'] = array(); + + TMDO_WooCommerce::register(); + + $hooks = $GLOBALS['_wp_filter_callbacks']; + $this->assertArrayNotHasKey( 'wpdo_register_fields', $hooks, 'no product / order / subscription zone fields' ); + $this->assertArrayNotHasKey( 'wpdo_register_entity_fields', $hooks, 'no user hot group' ); + $this->assertArrayHasKey( 'wpdo_register_custom_tables', $hooks, 'WooCommerce custom tables stay registered' ); + } }