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 ) {
@@ -66,7 +66,7 @@
@@ -77,7 +77,7 @@
' . $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'] );
+ }
}