diff --git a/includes/Actions.php b/includes/Actions.php index 90e30184..a1c33bbd 100644 --- a/includes/Actions.php +++ b/includes/Actions.php @@ -23,61 +23,22 @@ class Actions { * @since 1.5.6 */ public function __construct() { - add_action( 'wc_serial_numbers_key_db_data', array( __CLASS__, 'decrypt_key' ) ); - add_action( 'wc_serial_numbers_key_insert_data', array( __CLASS__, 'encrypt_key' ) ); - add_action( 'wc_serial_numbers_key_update_data', array( __CLASS__, 'encrypt_key' ) ); - add_action( 'wc_serial_numbers_key_insert', array( __CLASS__, 'enable_product' ) ); + add_action( 'wc_serial_numbers_key_inserted', array( __CLASS__, 'enable_product' ) ); add_action( 'wc_serial_numbers_key_deleted', array( __CLASS__, 'delete_activations' ) ); add_action( 'wc_serial_numbers_activation_inserted', array( __CLASS__, 'update_activation_count' ) ); add_action( 'wc_serial_numbers_activation_deleted', array( __CLASS__, 'update_activation_count' ) ); } /** - * Decrypt key. + * Enable serial numbers on the product when a key is inserted. * - * @param array $data The key data. + * @param Key $key The inserted key object. * * @since 1.4.6 */ - public static function decrypt_key( $data ) { - if ( ! empty( $data['serial_key'] ) ) { - $data['serial_key'] = wcsn_decrypt_key( $data['serial_key'] ); - } - - return $data; - } - - /** - * Encrypt key. - * - * @param array $data The key data. - * - * @since 1.4.6 - */ - public static function encrypt_key( $data ) { - if ( ! empty( $data['serial_key'] ) ) { - $data['serial_key'] = wcsn_encrypt_key( $data['serial_key'] ); - } - - return $data; - } - - /** - * Enable product. - * - * @param int $key_id The key ID. - * - * @since 1.4.6 - */ - public static function enable_product( $key_id ) { - $key = Key::find( $key_id ); - - if ( $key ) { - $product_id = $key->get_product_id(); - - if ( $product_id ) { - update_post_meta( $product_id, '_is_serial_number', 'yes' ); - } + public static function enable_product( $key ) { + if ( $key && $key->product_id ) { + update_post_meta( $key->product_id, '_is_serial_number', 'yes' ); } } @@ -89,7 +50,7 @@ public static function enable_product( $key_id ) { * @since 1.4.6 */ public static function delete_activations( $key ) { - $activations = $key->get_activations(); + $activations = $key->activations; if ( $activations ) { foreach ( $activations as $activation ) { $activation->delete(); @@ -121,7 +82,7 @@ public static function revoke_order_item_keys( $revoke ) { * @since 1.0.0 */ public static function update_activation_count( $activation ) { - $key = Key::find( $activation->get_serial_id() ); + $key = Key::find( $activation->serial_id ); if ( $key ) { $key->recount_remaining_activation(); } diff --git a/includes/Admin/ListTables/ActivationsTable.php b/includes/Admin/ListTables/ActivationsTable.php index 057c5d8c..418fc947 100644 --- a/includes/Admin/ListTables/ActivationsTable.php +++ b/includes/Admin/ListTables/ActivationsTable.php @@ -238,7 +238,7 @@ protected function column_instance( $activation ) { ); $actions['delete'] = sprintf( '%2$s', wp_nonce_url( $delete_url, 'bulk-activations' ), __( 'Delete', 'wc-serial-numbers' ) ); - return sprintf( '%1$s %2$s', esc_html( $activation->get_instance() ), $this->row_actions( $actions ) ); + return sprintf( '%1$s %2$s', esc_html( $activation->instance ), $this->row_actions( $actions ) ); } /** @@ -249,7 +249,7 @@ protected function column_instance( $activation ) { * @since 1.4.6 */ protected function column_product( $activation ) { - return esc_html( $activation->get_product_title() ); + return esc_html( $activation->product_title ); } /** @@ -260,9 +260,9 @@ protected function column_product( $activation ) { * @since 1.4.6 */ protected function column_serial_id( $activation ) { - $edit_url = admin_url( 'admin.php?page=wc-serial-numbers&id=' . $activation->get_serial_id() ); + $edit_url = admin_url( 'admin.php?page=wc-serial-numbers&id=' . $activation->serial_id ); - return sprintf( '#%2$s', esc_url( $edit_url ), esc_html( $activation->get_serial_id() ) ); + return sprintf( '#%2$s', esc_url( $edit_url ), esc_html( $activation->serial_id ) ); } /** @@ -273,7 +273,7 @@ protected function column_serial_id( $activation ) { * @since 1.4.6 */ protected function column_platform( $activation ) { - return empty( $activation->get_platform() ) ? '—' : esc_html( $activation->get_platform() ); + return empty( $activation->platform ) ? '—' : esc_html( $activation->platform ); } /** @@ -284,6 +284,6 @@ protected function column_platform( $activation ) { * @since 1.4.6 */ protected function column_activation_time( $activation ) { - return empty( $activation->get_activation_time() ) ? '—' : esc_html( $activation->get_activation_time() ); + return empty( $activation->activation_time ) ? '—' : esc_html( $activation->activation_time ); } } diff --git a/includes/Admin/ListTables/KeysTable.php b/includes/Admin/ListTables/KeysTable.php index e0be075a..a305e39a 100644 --- a/includes/Admin/ListTables/KeysTable.php +++ b/includes/Admin/ListTables/KeysTable.php @@ -451,7 +451,7 @@ protected function column_product( $item ) { * @since 1.4.6 */ protected function column_order( $item ) { - $order = $item->get_order(); + $order = $item->order; if ( empty( $order ) ) { return '—'; } @@ -515,17 +515,17 @@ protected function column_activation( $key ) { * @since 1.4.6 */ protected function column_valid_for( $key ) { - if ( ! empty( $key->get_validity() ) ) { + if ( ! empty( $key->validity ) ) { return wp_kses_post( sprintf( // translators: %1$s: validity, %2$s: validity. _n( '%s Day
After purchase', '%s Days
After purchase', - $key->get_validity(), + $key->validity, 'wc-serial-numbers' ), - number_format_i18n( $key->get_validity() ) + number_format_i18n( $key->validity ) ) ); } diff --git a/includes/Admin/Menus.php b/includes/Admin/Menus.php index 28ae264d..29e4f422 100644 --- a/includes/Admin/Menus.php +++ b/includes/Admin/Menus.php @@ -206,7 +206,7 @@ public function output_main_page() { $edit = isset( $_GET['edit'] ) ? absint( $_GET['edit'] ) : 0; if ( $edit ) { $key = Key::find( $edit ); - if ( ! $key->exists() ) { + if ( ! $key || ! $key->exists() ) { wp_safe_redirect( remove_query_arg( 'edit' ) ); exit(); } diff --git a/includes/Admin/Orders.php b/includes/Admin/Orders.php index dd135693..de54d701 100644 --- a/includes/Admin/Orders.php +++ b/includes/Admin/Orders.php @@ -265,19 +265,19 @@ public static function display_order_item_meta( $item_id, $item, $product ) { $data = array( 'key' => array( 'label' => __( 'Key', 'wc-serial-numbers' ), - 'value' => '' . $key->get_key() . '', + 'value' => '' . $key->key . '', ), 'expire_date' => array( 'label' => __( 'Expire date', 'wc-serial-numbers' ), - 'value' => $key->get_expire_date() ? $key->get_expire_date() : __( 'Lifetime', 'wc-serial-numbers' ), + 'value' => $key->expire_date ? $key->expire_date : __( 'Lifetime', 'wc-serial-numbers' ), ), 'activation_limit' => array( 'label' => __( 'Activation limit', 'wc-serial-numbers' ), - 'value' => $key->get_activation_limit() ? $key->get_activation_limit() : __( 'Unlimited', 'wc-serial-numbers' ), + 'value' => $key->activation_limit ? $key->activation_limit : __( 'Unlimited', 'wc-serial-numbers' ), ), 'status' => array( 'label' => __( 'Status', 'wc-serial-numbers' ), - 'value' => $key->get_status_label(), + 'value' => $key->status_label, ), ); @@ -303,7 +303,7 @@ public static function display_order_item_meta( $item_id, $item, $product ) { - + diff --git a/includes/Admin/Requests.php b/includes/Admin/Requests.php index 41cb14b0..ff457251 100644 --- a/includes/Admin/Requests.php +++ b/includes/Admin/Requests.php @@ -78,7 +78,7 @@ public static function handle_edit_key() { $add = empty( $data['id'] ) ? true : false; if ( $add ) { // Adding manually so let's enable to product and set the source. - $product_id = $key->get_product_id(); + $product_id = $key->product_id; update_post_meta( $product_id, '_is_serial_number', 'yes' ); update_post_meta( $product_id, '_serial_key_source', 'custom_source' ); @@ -87,7 +87,7 @@ public static function handle_edit_key() { WCSN()->add_notice( __( 'Key updated successfully.', 'wc-serial-numbers' ) ); } - $redirect_to = admin_url( 'admin.php?page=wc-serial-numbers&edit=' . $key->get_id() ); + $redirect_to = admin_url( 'admin.php?page=wc-serial-numbers&edit=' . $key->id ); wp_safe_redirect( $redirect_to ); exit; } diff --git a/includes/Admin/views/html-edit-key.php b/includes/Admin/views/html-edit-key.php index 47da7852..c582f095 100644 --- a/includes/Admin/views/html-edit-key.php +++ b/includes/Admin/views/html-edit-key.php @@ -41,8 +41,8 @@ %s', - esc_attr( $key->get_product_id() ), - esc_html( $key->get_product_title() ) + esc_attr( $key->product_id ), + esc_html( $key->product_title ) ); ?> @@ -56,7 +56,7 @@ - +

@@ -66,7 +66,7 @@
- +

@@ -77,7 +77,7 @@
- +
@@ -93,7 +93,7 @@

@@ -107,8 +107,8 @@ %s', - esc_attr( $key->get_order_id() ), - esc_html( $key->get_order_title() ) + esc_attr( $key->order_id ), + esc_html( $key->order_title ) ); ?> @@ -127,13 +127,13 @@
- get_order() ) : ?> + order ) : ?>

@@ -146,7 +146,7 @@ - get_order()->get_formatted_billing_full_name() ); ?> + order->get_formatted_billing_full_name() ); ?> @@ -154,7 +154,7 @@ - get_order()->get_billing_email() ); ?> + order->get_billing_email() ); ?> @@ -162,7 +162,7 @@ - get_order()->get_formatted_billing_address() ); ?> + order->get_formatted_billing_address() ); ?> @@ -171,13 +171,13 @@ - get_order()->get_billing_phone() ); ?> + order->get_billing_phone() ); ?>   - + @@ -192,7 +192,7 @@
- +
diff --git a/includes/Compat.php b/includes/Compat.php index 2900f96e..19e5e31c 100644 --- a/includes/Compat.php +++ b/includes/Compat.php @@ -77,27 +77,27 @@ public static function wpo_wcpdf_after_item_meta( $type, $item, $order ) { $data = array( 'key' => array( 'label' => __( 'Key', 'wc-serial-numbers' ), - 'value' => '' . $key->get_key() . '', + 'value' => '' . $key->key . '', ), 'expire_date' => array( 'label' => __( 'Expire date', 'wc-serial-numbers' ), - 'value' => $key->get_expire_date() ? $key->get_expire_date() : __( 'Lifetime', 'wc-serial-numbers' ), + 'value' => $key->expire_date ? $key->expire_date : __( 'Lifetime', 'wc-serial-numbers' ), ), 'activation_limit' => array( 'label' => __( 'Activation limit', 'wc-serial-numbers' ), - 'value' => $key->get_activation_limit() ? $key->get_activation_limit() : __( 'Unlimited', 'wc-serial-numbers' ), + 'value' => $key->activation_limit ? $key->activation_limit : __( 'Unlimited', 'wc-serial-numbers' ), ), 'activation_count' => array( 'label' => __( 'Activation count', 'wc-serial-numbers' ), - 'value' => $key->get_activation_count(), + 'value' => $key->activation_count, ), 'activation_email' => array( 'label' => __( 'Activation email', 'wc-serial-numbers' ), - 'value' => $key->get_customer_email(), + 'value' => $key->customer_email, ), 'status' => array( 'label' => __( 'Status', 'wc-serial-numbers' ), - 'value' => $key->get_status_label(), + 'value' => $key->status_label, ), ); diff --git a/includes/Deprecated/Functions.php b/includes/Deprecated/Functions.php index 9d1317cf..733c11eb 100644 --- a/includes/Deprecated/Functions.php +++ b/includes/Deprecated/Functions.php @@ -153,7 +153,7 @@ function wc_serial_numbers_update_serial_number_status( $id, $status ) { if ( ! $key ) { return new WP_Error( 'invalid_data', __( 'Serial number not found.', 'wc-serial-numbers' ) ); } - $key->set_status( $status ); + $key->status = $status; return $key->save(); } @@ -438,3 +438,58 @@ function wc_serial_numbers_control_order_table_columns( $columns ) { function wc_serial_numbers_validate_boolean( $string ) { return filter_var( $string, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); } + +/** + * Translate the legacy `customer_id` key query argument into a native condition. + * + * Keys have no customer column; the customer is derived from the order, so the + * argument is resolved to the customer's order IDs and applied as an + * `order_id IN (...)` condition. An empty list is left to the query layer, which + * ignores an empty IN rather than constraining the query. + * + * @deprecated 2.3.5 Pass a native `order_id__in` argument instead. + */ +add_filter( + 'wc_serial_numbers_key_query_args', + function ( $args, $query ) { + if ( empty( $args['customer_id'] ) ) { + return $args; + } + + $order_ids = wc_get_orders( + array( + 'customer_id' => absint( $args['customer_id'] ), + 'limit' => - 1, + 'return' => 'ids', + ) + ); + + $query->where( 'order_id', 'IN', $order_ids ); + unset( $args['customer_id'] ); + + return $args; + }, + 10, + 2 +); + +/** + * Drop a zero `product_id` key query argument. + * + * A key always has a real product, so a `product_id` of 0 means "any product" + * rather than a literal match. Without this the query layer would treat it as + * `product_id = 0` and return nothing, which is how the "All Products" export + * came back empty. + * + * @deprecated 2.3.5 Stop passing a zero `product_id` from the export. + */ +add_filter( + 'wc_serial_numbers_key_query_args', + function ( $args ) { + if ( isset( $args['product_id'] ) && empty( $args['product_id'] ) ) { + unset( $args['product_id'] ); + } + + return $args; + } +); diff --git a/includes/Functions/Template.php b/includes/Functions/Template.php index 1e39fb8b..8361a170 100644 --- a/includes/Functions/Template.php +++ b/includes/Functions/Template.php @@ -40,32 +40,32 @@ function wcsn_display_key_html( $key, $output = true ) { $properties = array( 'key' => array( 'label' => __( 'Key', 'wc-serial-numbers' ), - 'value' => '' . $key->get_key() . '', + 'value' => '' . $key->key . '', 'priority' => 10, ), 'activation_email' => array( 'label' => __( 'Activation Email', 'wc-serial-numbers' ), - 'value' => $key->get_customer_email(), + 'value' => $key->customer_email, 'priority' => 20, ), 'activation_limit' => array( 'label' => __( 'Activation Limit', 'wc-serial-numbers' ), - 'value' => ! empty( $key->get_activation_limit() ) ? number_format_i18n( $key->get_activation_limit() ) : __( 'None', 'wc-serial-numbers' ), + 'value' => ! empty( $key->activation_limit ) ? number_format_i18n( $key->activation_limit ) : __( 'None', 'wc-serial-numbers' ), 'priority' => 30, ), 'activation_count' => array( 'label' => __( 'Activation Count', 'wc-serial-numbers' ), - 'value' => ! empty( $key->get_activation_count() ) ? number_format_i18n( $key->get_activation_count() ) : __( 'None', 'wc-serial-numbers' ), + 'value' => ! empty( $key->activation_count ) ? number_format_i18n( $key->activation_count ) : __( 'None', 'wc-serial-numbers' ), 'priority' => 40, ), 'expire_date' => array( 'label' => __( 'Expire Date', 'wc-serial-numbers' ), - 'value' => ! empty( $key->get_expire_date() ) ? $key->get_expire_date() : __( 'Lifetime', 'wc-serial-numbers' ), + 'value' => ! empty( $key->expire_date ) ? $key->expire_date : __( 'Lifetime', 'wc-serial-numbers' ), 'priority' => 50, ), ); - $status = $key->get_status(); + $status = $key->status; if ( 'sold' === $status ) { $status = '' . __( 'Active', 'wc-serial-numbers' ) . ''; } elseif ( 'expired' === $status ) { diff --git a/includes/Models/Key.php b/includes/Models/Key.php index d99d2d5d..9e39f765 100644 --- a/includes/Models/Key.php +++ b/includes/Models/Key.php @@ -234,6 +234,10 @@ function ( $q ) use ( $columns, $search ) { * the serial key is decrypted when read from the database and search * terms are encrypted to match the value at rest. * + * The legacy `customer_id` and zero `product_id` query arguments are + * translated separately in includes/Deprecated/Functions.php so that + * compatibility shim can be removed without touching the model. + * * @since 1.0.0 * @return void */ diff --git a/includes/RestAPI.php b/includes/RestAPI.php index d9970a96..26b6cdd7 100644 --- a/includes/RestAPI.php +++ b/includes/RestAPI.php @@ -160,18 +160,18 @@ public function validate_request( $request ) { } // Check if the key has order ID. - if ( empty( $serial_key->get_order_id() ) || ( $serial_key->get_order_id() && ! get_post( $serial_key->get_order_id() ) ) ) { + if ( empty( $serial_key->order_id ) || ( $serial_key->order_id && ! get_post( $serial_key->order_id ) ) ) { return new \WP_Error( 'invalid_key', __( 'Serial key is not authorized to use.', 'wc-serial-numbers' ), array( 'status' => 400 ) ); } // Check if order status is completed. - $order = wc_get_order( $serial_key->get_order_id() ); + $order = wc_get_order( $serial_key->order_id ); if ( ! $order || ! apply_filters( 'wc_serial_numbers_api_validate_order_status', 'completed' === $order->get_status(), $order ) ) { return new \WP_Error( 'invalid_order', __( 'Please complete your order to activate the serial key.', 'wc-serial-numbers' ), array( 'status' => 400 ) ); } // Check if key is valid for the product. - if ( $serial_key->get_product_id() !== $product_id ) { + if ( $serial_key->product_id !== $product_id ) { return new \WP_Error( 'invalid_product_key', __( 'Serial key is not valid for this product.', 'wc-serial-numbers' ), array( 'status' => 400 ) ); } @@ -181,12 +181,12 @@ public function validate_request( $request ) { } // based on key status send response. - if ( 'expired' === $serial_key->get_status() ) { + if ( 'expired' === $serial_key->status ) { return new \WP_Error( 'expired_key', __( 'Serial key is expired.', 'wc-serial-numbers' ), array( 'status' => 400 ) ); - } elseif ( 'cancelled' === $serial_key->get_status() ) { + } elseif ( 'cancelled' === $serial_key->status ) { return new \WP_Error( 'key_cancelled', __( 'Serial key is cancelled.', 'wc-serial-numbers' ), array( 'status' => 400 ) ); - } elseif ( 'sold' !== $serial_key->get_status() ) { + } elseif ( 'sold' !== $serial_key->status ) { return new \WP_Error( 'invalid_key_status', __( 'Invalid serial key.', 'wc-serial-numbers' ), array( 'status' => 400 ) ); } @@ -214,22 +214,22 @@ public function validate_key( $request ) { $response = array( 'code' => 'key_valid', 'message' => __( 'Serial key is valid.', 'wc-serial-numbers' ), - 'activation_limit' => $serial_key->get_activation_limit(), - 'activation_count' => $serial_key->get_activation_count(), - 'activations_left' => $serial_key->get_activations_left(), - 'expire_date' => $serial_key->get_expire_date(), - 'status' => 'sold' === $serial_key->get_status() ? 'active' : $serial_key->get_status(), - 'product_id' => $serial_key->get_product_id(), - 'product' => $serial_key->get_product_title(), - 'activations' => $serial_key->get_activations( - array( - 'limit' => - 1, - 'output' => ARRAY_A, - ) + 'activation_limit' => $serial_key->activation_limit, + 'activation_count' => $serial_key->activation_count, + 'activations_left' => $serial_key->activations_left, + 'expire_date' => $serial_key->expire_date, + 'status' => 'sold' === $serial_key->status ? 'active' : $serial_key->status, + 'product_id' => $serial_key->product_id, + 'product' => $serial_key->product_title, + 'activations' => array_map( + static function ( $activation ) { + return $activation->to_array(); + }, + $serial_key->activations ), // Deprecated. - 'remaining' => $serial_key->get_activations_left(), + 'remaining' => $serial_key->activations_left, ); // send response. @@ -265,7 +265,7 @@ public function activate_key( $request ) { // Check if instance is already activated. $activation = Activation::find( array( - 'serial_id' => $serial_key->get_id(), + 'serial_id' => $serial_key->id, 'instance' => $instance, ) ); @@ -275,14 +275,14 @@ public function activate_key( $request ) { } // Check if key is already activated. - if ( $serial_key->get_activations_left() <= 0 ) { + if ( $serial_key->activations_left <= 0 ) { return new \WP_Error( 'no_activations_left', __( 'Activation limit reached.', 'wc-serial-numbers' ), array( 'status' => 400 ) ); } // Create activation. $activation = Activation::insert( array( - 'serial_id' => $serial_key->get_id(), + 'serial_id' => $serial_key->id, 'instance' => $instance, 'platform' => $platform, ) @@ -294,23 +294,23 @@ public function activate_key( $request ) { 'code' => 'key_activated', 'message' => __( 'Serial key is activated.', 'wc-serial-numbers' ), 'activated' => true, - 'instance' => $activation->get_instance(), - 'platform' => $activation->get_platform(), - 'activation_limit' => $serial_key->get_activation_limit(), - 'activation_count' => $serial_key->get_activation_count(), - 'activations_left' => $serial_key->get_activations_left(), - 'expires_at' => $serial_key->get_expire_date(), - 'product_id' => $serial_key->get_product_id(), - 'product' => $serial_key->get_product_title(), - 'activations' => $serial_key->get_activations( - array( - 'limit' => - 1, - 'output' => ARRAY_A, - ) + 'instance' => $activation->instance, + 'platform' => $activation->platform, + 'activation_limit' => $serial_key->activation_limit, + 'activation_count' => $serial_key->activation_count, + 'activations_left' => $serial_key->activations_left, + 'expires_at' => $serial_key->expire_date, + 'product_id' => $serial_key->product_id, + 'product' => $serial_key->product_title, + 'activations' => array_map( + static function ( $activation ) { + return $activation->to_array(); + }, + $serial_key->activations ), // Deprecated. - 'remaining' => $serial_key->get_activations_left(), + 'remaining' => $serial_key->activations_left, ); // send response. @@ -342,7 +342,7 @@ public function deactivate_key( $request ) { $activation = Activation::find( array( - 'serial_id' => $serial_key->get_id(), + 'serial_id' => $serial_key->id, 'instance' => $instance, ) ); @@ -358,22 +358,22 @@ public function deactivate_key( $request ) { 'code' => 'key_deactivated', 'message' => __( 'Serial key is deactivated.', 'wc-serial-numbers' ), 'deactivated' => true, - 'instance' => $activation->get_instance(), - 'activation_limit' => $serial_key->get_activation_limit(), - 'activation_count' => $serial_key->get_activation_count(), - 'activations_left' => $serial_key->get_activations_left(), - 'expires_at' => $serial_key->get_expire_date(), - 'product_id' => $serial_key->get_product_id(), - 'product' => $serial_key->get_product_title(), - 'activations' => $serial_key->get_activations( - array( - 'limit' => - 1, - 'output' => ARRAY_A, - ) + 'instance' => $activation->instance, + 'activation_limit' => $serial_key->activation_limit, + 'activation_count' => $serial_key->activation_count, + 'activations_left' => $serial_key->activations_left, + 'expires_at' => $serial_key->expire_date, + 'product_id' => $serial_key->product_id, + 'product' => $serial_key->product_title, + 'activations' => array_map( + static function ( $activation ) { + return $activation->to_array(); + }, + $serial_key->activations ), // Deprecated. - 'remaining' => $serial_key->get_activations_left(), + 'remaining' => $serial_key->activations_left, ); // send response. @@ -400,9 +400,9 @@ public function version_check( $request ) { $response = array( 'code' => 'version_checked', - 'product_id' => $serial_key->get_product_id(), - 'product' => $serial_key->get_product_title(), - 'version' => get_post_meta( $serial_key->get_product_id(), '_software_version', true ), + 'product_id' => $serial_key->product_id, + 'product' => $serial_key->product_title, + 'version' => get_post_meta( $serial_key->product_id, '_software_version', true ), ); // send response. diff --git a/includes/Stocks.php b/includes/Stocks.php index 663da5bc..a302afc0 100644 --- a/includes/Stocks.php +++ b/includes/Stocks.php @@ -61,7 +61,7 @@ public static function update_stocks( $key ) { return; // Return if stock management is disabled. } - $product = $key->get_product(); + $product = $key->product; // Check if product exists and stock management is enabled. if ( ! $product || ! $product->get_manage_stock() ) { diff --git a/includes/functions.php b/includes/functions.php index ccb489b9..de3b4f0d 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -422,7 +422,7 @@ function wcsn_order_get_unfulfilled_items( $order_id ) { foreach ( $line_items as $line_item ) { $qty = $line_item['quantity']; foreach ( $keys as $key ) { - if ( $key->get_product_id() !== $line_item['product_id'] ) { + if ( $key->product_id !== $line_item['product_id'] ) { continue; } // todo uncomment this when we will support for order item id. @@ -600,8 +600,8 @@ function wcsn_order_update_keys( $order_id ) { foreach ( $keys as $key ) { $is_expired = $key->is_expired(); - if ( $is_expired && 'expired' !== $key->get_status() ) { - $key->set_status( 'expired' ); + if ( $is_expired && 'expired' !== $key->status ) { + $key->status = 'expired'; $key->save(); } elseif ( ! $is_expired && in_array( $order_status, @@ -610,11 +610,11 @@ function wcsn_order_update_keys( $order_id ) { 'complete', ), true - ) && 'sold' !== $key->get_status() ) { - $key->set_status( 'sold' ); + ) && 'sold' !== $key->status ) { + $key->status = 'sold'; $key->save(); - } elseif ( 'on-hold' === $order_status && ! $is_expired && 'pending' !== $key->get_status() ) { - $key->set_status( 'pending' ); + } elseif ( 'on-hold' === $order_status && ! $is_expired && 'pending' !== $key->status ) { + $key->status = 'pending'; $key->save(); } elseif ( in_array( $order_status, $revoke_statues, true ) && ! $is_expired && apply_filters( 'wc_serial_numbers_revoke_order_item_keys', true, $line_items, $order_id ) ) { wcsn_order_remove_keys( $order_id ); @@ -753,7 +753,7 @@ function wcsn_order_replace_key( $order_id, $product_id = null, $key_id = null ) $props['order_date'] = null; } $key->fill( $props ); - if ( $key->save() ) { + if ( ! is_wp_error( $key->save() ) ) { ++$replaced; } } @@ -1013,32 +1013,32 @@ function wcsn_get_key_display_properties( $key, $context = 'order_details' ) { $properties = array( 'key' => array( 'label' => __( 'Key', 'wc-serial-numbers' ), - 'value' => '' . $key->get_key() . '', + 'value' => '' . $key->key . '', 'priority' => 10, ), 'activation_email' => array( 'label' => __( 'Activation Email', 'wc-serial-numbers' ), - 'value' => $key->get_customer_email(), + 'value' => $key->customer_email, 'priority' => 20, ), 'activation_limit' => array( 'label' => __( 'Activation Limit', 'wc-serial-numbers' ), - 'value' => ! empty( $key->get_activation_limit() ) ? number_format_i18n( $key->get_activation_limit() ) : __( 'None', 'wc-serial-numbers' ), + 'value' => ! empty( $key->activation_limit ) ? number_format_i18n( $key->activation_limit ) : __( 'None', 'wc-serial-numbers' ), 'priority' => 30, ), 'activation_count' => array( 'label' => __( 'Activation Count', 'wc-serial-numbers' ), - 'value' => ! empty( $key->get_activation_count() ) ? number_format_i18n( $key->get_activation_count() ) : __( 'None', 'wc-serial-numbers' ), + 'value' => ! empty( $key->activation_count ) ? number_format_i18n( $key->activation_count ) : __( 'None', 'wc-serial-numbers' ), 'priority' => 40, ), 'expire_date' => array( 'label' => __( 'Expire Date', 'wc-serial-numbers' ), - 'value' => ! empty( $key->get_expire_date() ) ? $key->get_expire_date() : __( 'Lifetime', 'wc-serial-numbers' ), + 'value' => ! empty( $key->expire_date ) ? $key->expire_date : __( 'Lifetime', 'wc-serial-numbers' ), 'priority' => 50, ), 'status' => array( 'label' => __( 'Status', 'wc-serial-numbers' ), - 'value' => $key->get_status(), + 'value' => $key->status, 'priority' => 100, ), ); diff --git a/tests/phpunit/KeyModelTest.php b/tests/phpunit/KeyModelTest.php index 988153ce..2872b136 100644 --- a/tests/phpunit/KeyModelTest.php +++ b/tests/phpunit/KeyModelTest.php @@ -124,4 +124,19 @@ public function testExpiryAndActivationsLeftMath(): void { $limited->set_activation_count( 2 ); $this->assertSame( 3, $limited->get_activations_left() ); } + + /** + * Regression #527: find() returns null (not a non-existent object) for a + * missing id. The admin edit screen must null-guard the result before + * calling ->exists(), or it fatals on a stale/deleted edit link. + */ + public function testFindReturnsNullForMissingId(): void { + $this->assertNull( Key::find( 999999 ) ); + + $product = $this->create_product(); + $key = $this->make_available_key( $product->get_id(), 'FIND-ME' ); + $found = Key::find( $key->get_id() ); + $this->assertInstanceOf( Key::class, $found ); + $this->assertTrue( $found->exists() ); + } } diff --git a/tests/phpunit/ListTableQueryTest.php b/tests/phpunit/ListTableQueryTest.php index 7597699e..52cef084 100644 --- a/tests/phpunit/ListTableQueryTest.php +++ b/tests/phpunit/ListTableQueryTest.php @@ -121,6 +121,58 @@ public function testIncludeArgRestrictsToId(): void { $this->assertSame( $target->get_id(), $items[0]->get_id() ); } + /** + * The customer_id filter restricts results to keys on that customer's orders. + * + * Regression: keys have no customer column, so the filter must resolve the + * customer to their order ids and apply an order_id IN (...) condition + * (Key::prepare_customer_query). Without it the arg is silently dropped and + * every key leaks into the filtered view and its per-status counts. + */ + public function testCustomerIdFilterRestrictsToCustomersKeys(): void { + $product = $this->create_product(); + $customer_a = $this->create_customer( 'a@example.com' ); + $customer_b = $this->create_customer( 'b@example.com' ); + + $order_a = $this->create_order( $product, 1, array( 'customer_id' => $customer_a, 'status' => 'completed' ) ); + $order_b = $this->create_order( $product, 1, array( 'customer_id' => $customer_b, 'status' => 'completed' ) ); + + Key::insert( array( 'product_id' => $product->get_id(), 'serial_key' => 'CUST-A1', 'status' => 'sold', 'order_id' => $order_a->get_id() ) ); + Key::insert( array( 'product_id' => $product->get_id(), 'serial_key' => 'CUST-A2', 'status' => 'sold', 'order_id' => $order_a->get_id() ) ); + Key::insert( array( 'product_id' => $product->get_id(), 'serial_key' => 'CUST-B1', 'status' => 'sold', 'order_id' => $order_b->get_id() ) ); + $this->make_available_key( $product->get_id(), 'CUST-NONE' ); + + $items = Key::query( $this->table_args( array( 'customer_id' => $customer_a ) ) ); + + $this->assertCount( 2, $items ); + foreach ( $items as $item ) { + $this->assertSame( $order_a->get_id(), $item->order_id ); + } + + $this->assertSame( 2, Key::count( array_merge( $this->table_args(), array( 'customer_id' => $customer_a ) ) ) ); + $this->assertSame( 1, Key::count( array_merge( $this->table_args(), array( 'customer_id' => $customer_b ) ) ) ); + } + + /** + * A customer with no orders resolves to an empty order-id list, which the + * query layer ignores (empty IN is not a constraint). The filter therefore + * falls through to the unfiltered result set rather than forcing zero rows. + * + * This pins the framework's empty-IN contract: in the admin the customer + * filter is only reachable from an existing key, so this edge is benign. + */ + public function testCustomerIdFilterWithNoOrdersIsIgnored(): void { + $product = $this->create_product(); + $this->make_available_key( $product->get_id(), 'CUST-X1' ); + $this->make_available_key( $product->get_id(), 'CUST-X2' ); + $customer = $this->create_customer( 'noorders@example.com' ); + + $items = Key::query( $this->table_args( array( 'customer_id' => $customer ) ) ); + + $this->assertCount( 2, $items ); + $this->assertSame( 2, Key::count( array_merge( $this->table_args(), array( 'customer_id' => $customer ) ) ) ); + } + /** * per_page/paged paginate into distinct slices like the table expects. */ diff --git a/tests/phpunit/ModelHooksTest.php b/tests/phpunit/ModelHooksTest.php index 5c7fa552..951d1925 100644 --- a/tests/phpunit/ModelHooksTest.php +++ b/tests/phpunit/ModelHooksTest.php @@ -42,6 +42,23 @@ function ( $model, $data ) use ( &$captured ) { $this->assertSame( $key->get_id(), $captured[0][1]['id'] ); } + /** + * Inserting a key auto-enables serial numbers on its product. + * + * Regression: the b8 base fires `..._key_inserted` (passing the model), not + * the legacy `..._key_insert` (passing an id). Actions::enable_product must + * stay wired to the live hook so programmatic inserts (e.g. Pro CSV import) + * still mark the product as serial-enabled. + */ + public function testKeyInsertEnablesProductViaHook(): void { + $product = $this->create_product( array( 'serial_enabled' => false ) ); + $this->assertSame( '', get_post_meta( $product->get_id(), '_is_serial_number', true ) ); + + $this->make_available_key( $product->get_id(), 'HOOK-ENABLE-1' ); + + $this->assertSame( 'yes', get_post_meta( $product->get_id(), '_is_serial_number', true ) ); + } + /** * key_updated fires with the model and only the changed columns. */ diff --git a/tests/phpunit/ProContractTest.php b/tests/phpunit/ProContractTest.php index 573196aa..cb8b7950 100644 --- a/tests/phpunit/ProContractTest.php +++ b/tests/phpunit/ProContractTest.php @@ -141,6 +141,35 @@ public function testExportQueryShape(): void { $this->assertSame( array(), $next_page, 'An out-of-range page must return empty so the export loop terminates.' ); } + /** + * Regression #532: the Pro "Export All" builds its query with product_id => 0 + * ("All Products") and no status. The query layer would treat 0 as a literal + * product_id match and return nothing (blank CSV); Key::prepare_product_query + * must drop the zero arg so every key is exported. + */ + public function testExportAllProductsAllStatusReturnsRows(): void { + $product_a = $this->create_product(); + $product_b = $this->create_product(); + $order = $this->create_order( $product_a, 1, array( 'status' => 'completed' ) ); + + $this->make_available_key( $product_a->get_id(), 'EXP-ALL-A1' ); + $sold = Key::insert( + array( + 'product_id' => $product_b->get_id(), + 'serial_key' => 'EXP-ALL-B1', + 'status' => 'sold', + 'order_id' => $order->get_id(), + ) + ); + $this->assertNotWPError( $sold ); + + // The exact arg shape the Pro export passes for All Products + All Status. + $results = Key::query( array( 'product_id' => 0, 'per_page' => 20, 'paged' => 1 ) ); + + $this->assertCount( 2, $results ); + $this->assertSame( 2, Key::count( array( 'product_id' => 0 ) ) ); + } + /** * Released Pro 1.4.6 builds its CSV-export date range as a legacy `where_query` * array of array( 'column', 'compare', 'value' ) clauses (Admin/Actions.php), diff --git a/tests/phpunit/RestApiValidateTest.php b/tests/phpunit/RestApiValidateTest.php index cd703bb4..3e31031e 100644 --- a/tests/phpunit/RestApiValidateTest.php +++ b/tests/phpunit/RestApiValidateTest.php @@ -3,6 +3,7 @@ namespace WooCommerceSerialNumbers\Tests; +use WooCommerceSerialNumbers\Models\Activation; use WooCommerceSerialNumbers\Models\Key; /** @@ -145,4 +146,36 @@ public function testValidSoldKeyReturnsContract(): void { $this->assertArrayHasKey( 'activations_left', $data ); $this->assertArrayHasKey( 'expire_date', $data ); } + + /** + * Regression #531: the `activations` field must serialize as a list of + * associative arrays (the documented contract the Legacy API add-on parses), + * not model objects that JSON-encode to empty `{}` and fatal the client. + */ + public function testActivationsFieldSerializesAsArrays(): void { + list( $product, , $key ) = $this->make_bound_key( 'ACT-CONTRACT-1' ); + + $activation = Activation::insert( + array( + 'serial_id' => $key->get_id(), + 'instance' => 'instance-1', + 'platform' => 'linux', + ) + ); + $this->assertNotWPError( $activation ); + + $response = rest_do_request( $this->get_rest_request( '/wcsn/validate', array( 'product_id' => $product->get_id(), 'serial_key' => 'ACT-CONTRACT-1' ) ) ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertIsArray( $data['activations'] ); + $this->assertCount( 1, $data['activations'] ); + $this->assertIsArray( $data['activations'][0], 'Each activation must be an array, not a model object.' ); + $this->assertSame( 'instance-1', $data['activations'][0]['instance'] ); + $this->assertSame( 'linux', $data['activations'][0]['platform'] ); + + // The whole payload must round-trip through JSON without emptying the activations. + $decoded = json_decode( wp_json_encode( $data ), true ); + $this->assertSame( 'instance-1', $decoded['activations'][0]['instance'] ); + } }