From 60150476ced74fb6f949f00dd2b5d955bc49fbe5 Mon Sep 17 00:00:00 2001 From: sultann Date: Thu, 11 Jun 2026 00:43:21 +0600 Subject: [PATCH 1/5] Reorganize the codebase into concern-based components and move views to templates --- includes/Activations/Activations.php | 53 +++ includes/Activations/Admin.php | 36 ++ .../ListTable.php} | 6 +- includes/Admin/Admin.php | 36 +- includes/Admin/Menus.php | 344 +----------------- includes/Admin/Reports.php | 63 ++++ includes/Admin/Tools.php | 251 +++++++++++++ includes/Cache.php | 39 -- includes/Cron.php | 75 ---- includes/Frontend/Frontend.php | 60 --- .../{Admin/Requests.php => Keys/Admin.php} | 45 ++- includes/{Actions.php => Keys/Keys.php} | 79 ++-- .../KeysTable.php => Keys/ListTable.php} | 6 +- .../{Admin/Orders.php => Orders/Admin.php} | 18 +- includes/{ => Orders}/Orders.php | 14 +- includes/Plugin.php | 12 +- includes/{Admin => }/Products.php | 14 +- .../{Frontend/Shortcodes.php => Shop.php} | 33 +- .../StockTable.php => Stocks/ListTable.php} | 6 +- includes/{ => Stocks}/Stocks.php | 44 ++- .../admin/api-actions.php | 0 .../admin/api-validation.php | 0 .../admin/edit-key.php | 0 .../admin/list-activations.php | 2 +- .../admin/list-keys.php | 2 +- .../admin/list-stock.php | 2 +- .../admin/notices/dependency.php | 0 .../admin/reports.php | 0 .../admin/tools.php | 0 29 files changed, 629 insertions(+), 611 deletions(-) create mode 100644 includes/Activations/Activations.php create mode 100644 includes/Activations/Admin.php rename includes/{Admin/ListTables/ActivationsTable.php => Activations/ListTable.php} (97%) create mode 100644 includes/Admin/Reports.php create mode 100644 includes/Admin/Tools.php delete mode 100644 includes/Cache.php delete mode 100644 includes/Cron.php delete mode 100644 includes/Frontend/Frontend.php rename includes/{Admin/Requests.php => Keys/Admin.php} (90%) rename includes/{Actions.php => Keys/Keys.php} (62%) rename includes/{Admin/ListTables/KeysTable.php => Keys/ListTable.php} (99%) rename includes/{Admin/Orders.php => Orders/Admin.php} (97%) rename includes/{ => Orders}/Orders.php (94%) rename includes/{Admin => }/Products.php (97%) rename includes/{Frontend/Shortcodes.php => Shop.php} (92%) rename includes/{Admin/ListTables/StockTable.php => Stocks/ListTable.php} (97%) rename includes/{ => Stocks}/Stocks.php (59%) rename includes/Admin/views/html-api-actions.php => templates/admin/api-actions.php (100%) rename includes/Admin/views/html-api-validation.php => templates/admin/api-validation.php (100%) rename includes/Admin/views/html-edit-key.php => templates/admin/edit-key.php (100%) rename includes/Admin/views/html-list-activations.php => templates/admin/list-activations.php (90%) rename includes/Admin/views/html-list-keys.php => templates/admin/list-keys.php (95%) rename includes/Admin/views/html-list-stock.php => templates/admin/list-stock.php (85%) rename includes/Admin/views/html-notice-dependency.php => templates/admin/notices/dependency.php (100%) rename includes/Admin/views/html-reports.php => templates/admin/reports.php (100%) rename includes/Admin/views/html-tools.php => templates/admin/tools.php (100%) diff --git a/includes/Activations/Activations.php b/includes/Activations/Activations.php new file mode 100644 index 00000000..f8e55500 --- /dev/null +++ b/includes/Activations/Activations.php @@ -0,0 +1,53 @@ + + */ + public array $components = array( + Admin::class, + ); + + /** + * Register hooks. + * + * @since 1.0.0 + * @return void + */ + public function register(): void { + add_action( 'wc_serial_numbers_activation_inserted', array( $this, 'update_activation_count' ) ); + add_action( 'wc_serial_numbers_activation_deleted', array( $this, 'update_activation_count' ) ); + } + + /** + * Update activation count. + * + * @param Activation $activation The activation object. + * + * @since 1.0.0 + */ + public function update_activation_count( $activation ) { + $key = Key::get( $activation->get_serial_id() ); + if ( $key ) { + $key->recount_remaining_activation(); + } + } +} diff --git a/includes/Activations/Admin.php b/includes/Activations/Admin.php new file mode 100644 index 00000000..d2c19ba7 --- /dev/null +++ b/includes/Activations/Admin.php @@ -0,0 +1,36 @@ +app->template->view( 'admin.list-activations' ); + } +} diff --git a/includes/Admin/ListTables/ActivationsTable.php b/includes/Activations/ListTable.php similarity index 97% rename from includes/Admin/ListTables/ActivationsTable.php rename to includes/Activations/ListTable.php index eeb078f4..3a803143 100644 --- a/includes/Admin/ListTables/ActivationsTable.php +++ b/includes/Activations/ListTable.php @@ -1,6 +1,8 @@ id ) ? $screen->id : ''; - } - - // Ensure the table handler is only loaded once. Prevents multiple loads if a plugin calls check_ajax_referer many times. - remove_action( 'current_screen', array( $this, 'setup_screen' ) ); - remove_action( 'check_ajax_referer', array( $this, 'setup_screen' ) ); - } - - /** - * Validate screen options on update. - * - * @param bool|int $status Screen option value. Default false to skip. - * @param string $option The option name. - * @param int $value The number of rows to use. - */ - public function save_screen_options( $status, $option, $value ) { - if ( in_array( $option, array( 'wsn_keys_per_page', 'wsn_generators_per_page', 'wsn_activations_per_page' ), true ) ) { - return $value; - } - - return $status; } /** @@ -102,7 +60,7 @@ public function main_menu() { __( 'Serial Keys', 'wc-serial-numbers' ), 'manage_woocommerce', // phpcs:ignore WordPress.WP.Capabilities.Unknown 'wc-serial-numbers', - array( $this, 'output_main_page' ) + $this->app->callback( array( Keys\Admin::class, 'output_page' ) ) ); } @@ -122,7 +80,7 @@ public function activations_menu() { __( 'Activations', 'wc-serial-numbers' ), 'manage_woocommerce', // phpcs:ignore WordPress.WP.Capabilities.Unknown 'wc-serial-numbers-activations', - array( $this, 'output_activations_page' ) + $this->app->callback( array( Activations\Admin::class, 'output_page' ) ) ); } @@ -139,7 +97,7 @@ public function tools_menu() { __( 'Tools', 'wc-serial-numbers' ), 'manage_woocommerce', // phpcs:ignore WordPress.WP.Capabilities.Unknown 'wc-serial-numbers-tools', - array( $this, 'output_tools_page' ) + $this->app->callback( array( Tools::class, 'output_page' ) ) ); } @@ -156,7 +114,7 @@ public function reports_menu() { __( 'Reports', 'wc-serial-numbers' ), 'manage_woocommerce', // phpcs:ignore WordPress.WP.Capabilities.Unknown 'wc-serial-numbers-reports', - array( $this, 'output_reports_page' ) + $this->app->callback( array( Reports::class, 'output_page' ) ) ); } @@ -196,107 +154,6 @@ public function promo_menu() { } } - /** - * Output keys page. - * - * @since 1.0.0 - * @return void - */ - public function output_main_page() { - wp_verify_nonce( '_nonce' ); - $add = isset( $_GET['add'] ) ? true : false; - $edit = isset( $_GET['edit'] ) ? absint( $_GET['edit'] ) : 0; - if ( $edit ) { - $key = new Key( $edit ); - if ( ! $key->exists() ) { - wp_safe_redirect( remove_query_arg( 'edit' ) ); - exit(); - } - } - - if ( $add ) { - $key = new Key(); - include __DIR__ . '/views/html-edit-key.php'; - } elseif ( $edit ) { - include __DIR__ . '/views/html-edit-key.php'; - } else { - include __DIR__ . '/views/html-list-keys.php'; - } - } - - /** - * Output activations page. - * - * @since 1.0.0 - * @return void - */ - public function output_activations_page() { - Admin::view( 'html-list-activations.php' ); - } - - - /** - * Output tools page. - * - * @since 1.0.0 - * @return void - */ - public function output_tools_page() { - wp_verify_nonce( '_nonce' ); - $tabs = array( - 'generators' => __( 'Generators', 'wc-serial-numbers' ), - 'api' => __( 'API Toolkit', 'wc-serial-numbers' ), - 'import' => __( 'Import', 'wc-serial-numbers' ), - 'export' => __( 'Export', 'wc-serial-numbers' ), - ); - - // If software support is disabled, remove the activations tab. - if ( ! wcsn_is_software_support_enabled() ) { - unset( $tabs['api'] ); - } - - $tabs = apply_filters( 'wc_serial_numbers_tools_tabs', $tabs ); - $tab_ids = array_keys( $tabs ); - $current_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : reset( $tab_ids ); - $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; - - Admin::view( - 'html-tools.php', - array( - 'tabs' => $tabs, - 'current_tab' => $current_tab, - 'page' => $page, - ) - ); - } - - /** - * Output reports page. - * - * @since 1.0.0 - * @return void - */ - public function output_reports_page() { - wp_verify_nonce( '_nonce' ); - $tabs = array( - 'stock' => __( 'Stock', 'wc-serial-numbers' ), - ); - - $tabs = apply_filters( 'wc_serial_numbers_reports_tabs', $tabs ); - $tab_ids = array_keys( $tabs ); - $current_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : reset( $tab_ids ); - $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; - - Admin::view( - 'html-reports.php', - array( - 'tabs' => $tabs, - 'current_tab' => $current_tab, - 'page' => $page, - ) - ); - } - /** * Redirect to pro page. * @@ -311,19 +168,6 @@ public function go_pro_redirect() { } } - /** - * Add status tab. - * - * @param array $tabs Tabs. - * - * @return array - */ - public static function add_tools_status_tab( $tabs ) { - $tabs['status'] = __( 'Status', 'wc-serial-numbers' ); - - return $tabs; - } - /** * Import tab content. * @@ -331,22 +175,7 @@ public static function add_tools_status_tab( $tabs ) { * @return void */ public static function import_tab() { - ?> -
-
-

- -
- <?php esc_attr_e( 'Import Serial Numbers', 'wc-serial-numbers' ); ?> -
-
-
-

- -
- <?php esc_attr_e( 'Import Serial Numbers', 'wc-serial-numbers' ); ?> -
- make( Tools::class )->import_tab(); } /** @@ -356,163 +185,16 @@ public static function import_tab() { * @return void */ public static function export_tab() { - ?> -
-
-

- -
- <?php esc_attr_e( 'Export Serial Numbers', 'wc-serial-numbers' ); ?> -
- make( Tools::class )->export_tab(); } /** - * Getnerators tab content. + * Generators tab content. * * @since 1.4.6 * @return void */ public static function generators_tab() { - ?> -
-
-

- -
- <?php esc_attr_e( 'Generators', 'wc-serial-numbers' ); ?> -
- WCSN()->version, - ); - if ( WCSN()->is_pro_active() && function_exists( 'wc_serial_numbers_pro' ) ) { - $statuses['Serial Numbers Pro version'] = WCSN_PRO()->get_version(); - } - - // Check if required tables exist. - $required_tables = array( - 'serial_numbers', - 'serial_numbers_activations', - ); - foreach ( $required_tables as $table ) { - $exists = $GLOBALS['wpdb']->get_var( $GLOBALS['wpdb']->prepare( 'SHOW TABLES LIKE %s', $GLOBALS['wpdb']->prefix . $table ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - if ( $exists ) { - $statuses[ $table ] = __( 'Table exists', 'wc-serial-numbers' ); - } else { - $statuses[ $table ] = __( 'Table does not exist', 'wc-serial-numbers' ); - } - } - - // Cron jobs. - $cron_jobs = array( - 'wc_serial_numbers_hourly_event' => __( 'Hourly cron', 'wc-serial-numbers' ), - 'wc_serial_numbers_daily_event' => __( 'Daily cron', 'wc-serial-numbers' ), - ); - foreach ( $cron_jobs as $cron_job => $cron_job_name ) { - $next_scheduled = wp_next_scheduled( $cron_job ); - if ( $next_scheduled ) { - // translators: %s: Next scheduled time. - $statuses[ $cron_job_name ] = sprintf( __( 'Next run: %s', 'wc-serial-numbers' ), esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $next_scheduled ) ) ); - } else { - $statuses[ $cron_job_name ] = __( 'Not scheduled', 'wc-serial-numbers' ); - } - } - $statuses = apply_filters( 'wc_serial_numbers_plugin_statuses', $statuses ); - ?> - - - - - - - - $value ) : ?> - - - - - - - - -

- - - 1, - 'fields' => 'ids', - ) - ); - $the_query = new \WP_Query( $args ); - $product_ids = $the_query->get_posts(); - $products = array(); - foreach ( $product_ids as $product_id ) { - $product = wc_get_product( $product_id ); - if ( ! $product ) { - continue; - } - $products[ $product->get_id() ] = sprintf( '%s (#%d)', $product->get_name(), $product->get_id() ); - } - - Admin::view( 'html-api-validation', array( 'products' => $products ) ); - } - - /** - * Activation deactivation section. - * - * @since 1.4.6 - * @return void - */ - public static function api_activation_deactivation_section() { - $args = array_merge( - wcsn_get_products_query_args(), - array( - 'posts_per_page' => - 1, - 'fields' => 'ids', - ) - ); - $the_query = new \WP_Query( $args ); - $product_ids = $the_query->get_posts(); - $products = array(); - foreach ( $product_ids as $product_id ) { - $product = wc_get_product( $product_id ); - if ( ! $product ) { - continue; - } - $products[ $product->get_id() ] = sprintf( '%s (#%d)', $product->get_name(), $product->get_id() ); - } - - Admin::view( 'html-api-actions', array( 'products' => $products ) ); - } - - /** - * Stock section. - * - * @since 1.4.6 - * @return void - */ - public static function reports_stock_tab() { - Admin::view( 'html-list-stock' ); + WCSN()->make( Tools::class )->generators_tab(); } } diff --git a/includes/Admin/Reports.php b/includes/Admin/Reports.php new file mode 100644 index 00000000..b2b1fc28 --- /dev/null +++ b/includes/Admin/Reports.php @@ -0,0 +1,63 @@ + __( 'Stock', 'wc-serial-numbers' ), + ); + + $tabs = apply_filters( 'wc_serial_numbers_reports_tabs', $tabs ); + $tab_ids = array_keys( $tabs ); + $current_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : reset( $tab_ids ); + $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; + + $this->app->template->view( + 'admin.reports', + array( + 'tabs' => $tabs, + 'current_tab' => $current_tab, + 'page' => $page, + ) + ); + } + + /** + * Stock section. + * + * @since 1.4.6 + * @return void + */ + public function stock_tab() { + $this->app->template->view( 'admin.list-stock' ); + } +} diff --git a/includes/Admin/Tools.php b/includes/Admin/Tools.php new file mode 100644 index 00000000..3428337e --- /dev/null +++ b/includes/Admin/Tools.php @@ -0,0 +1,251 @@ + __( 'Generators', 'wc-serial-numbers' ), + 'api' => __( 'API Toolkit', 'wc-serial-numbers' ), + 'import' => __( 'Import', 'wc-serial-numbers' ), + 'export' => __( 'Export', 'wc-serial-numbers' ), + ); + + // If software support is disabled, remove the activations tab. + if ( ! wcsn_is_software_support_enabled() ) { + unset( $tabs['api'] ); + } + + $tabs = apply_filters( 'wc_serial_numbers_tools_tabs', $tabs ); + $tab_ids = array_keys( $tabs ); + $current_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : reset( $tab_ids ); + $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; + + $this->app->template->view( + 'admin.tools', + array( + 'tabs' => $tabs, + 'current_tab' => $current_tab, + 'page' => $page, + ) + ); + } + + /** + * Add status tab. + * + * @param array $tabs Tabs. + * + * @return array + */ + public function add_status_tab( $tabs ) { + $tabs['status'] = __( 'Status', 'wc-serial-numbers' ); + + return $tabs; + } + + /** + * Import tab content. + * + * @since 1.0.0 + * @return void + */ + public function import_tab() { + ?> +
+
+

+ +
+ <?php esc_attr_e( 'Import Serial Numbers', 'wc-serial-numbers' ); ?> +
+
+
+

+ +
+ <?php esc_attr_e( 'Import Serial Numbers', 'wc-serial-numbers' ); ?> +
+ +
+
+

+ +
+ <?php esc_attr_e( 'Export Serial Numbers', 'wc-serial-numbers' ); ?> +
+ +
+
+

+ +
+ <?php esc_attr_e( 'Generators', 'wc-serial-numbers' ); ?> +
+ $this->app->version, + ); + if ( $this->app->is_pro_active() && function_exists( 'wc_serial_numbers_pro' ) ) { + $statuses['Serial Numbers Pro version'] = WCSN_PRO()->get_version(); + } + + // Check if required tables exist. + $required_tables = array( + 'serial_numbers', + 'serial_numbers_activations', + ); + foreach ( $required_tables as $table ) { + $exists = $GLOBALS['wpdb']->get_var( $GLOBALS['wpdb']->prepare( 'SHOW TABLES LIKE %s', $GLOBALS['wpdb']->prefix . $table ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + if ( $exists ) { + $statuses[ $table ] = __( 'Table exists', 'wc-serial-numbers' ); + } else { + $statuses[ $table ] = __( 'Table does not exist', 'wc-serial-numbers' ); + } + } + + // Cron jobs. + $cron_jobs = array( + 'wc_serial_numbers_hourly_event' => __( 'Hourly cron', 'wc-serial-numbers' ), + 'wc_serial_numbers_daily_event' => __( 'Daily cron', 'wc-serial-numbers' ), + ); + foreach ( $cron_jobs as $cron_job => $cron_job_name ) { + $next_scheduled = wp_next_scheduled( $cron_job ); + if ( $next_scheduled ) { + // translators: %s: Next scheduled time. + $statuses[ $cron_job_name ] = sprintf( __( 'Next run: %s', 'wc-serial-numbers' ), esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $next_scheduled ) ) ); + } else { + $statuses[ $cron_job_name ] = __( 'Not scheduled', 'wc-serial-numbers' ); + } + } + $statuses = apply_filters( 'wc_serial_numbers_plugin_statuses', $statuses ); + ?> + + + + + + + + $value ) : ?> + + + + + + + + +

+ + app->template->view( 'admin.api-validation', array( 'products' => $this->get_key_products() ) ); + } + + /** + * Activation deactivation section. + * + * @since 1.4.6 + * @return void + */ + public function api_activation_deactivation_section() { + $this->app->template->view( 'admin.api-actions', array( 'products' => $this->get_key_products() ) ); + } + + /** + * Get the key enabled products as id => label pairs. + * + * @since 1.4.6 + * @return array + */ + protected function get_key_products() { + $args = array_merge( + wcsn_get_products_query_args(), + array( + 'posts_per_page' => - 1, + 'fields' => 'ids', + ) + ); + $the_query = new \WP_Query( $args ); + $product_ids = $the_query->get_posts(); + $products = array(); + foreach ( $product_ids as $product_id ) { + $product = wc_get_product( $product_id ); + if ( ! $product ) { + continue; + } + $products[ $product->get_id() ] = sprintf( '%s (#%d)', $product->get_name(), $product->get_id() ); + } + + return $products; + } +} diff --git a/includes/Cache.php b/includes/Cache.php deleted file mode 100644 index e5e92ef3..00000000 --- a/includes/Cache.php +++ /dev/null @@ -1,39 +0,0 @@ -query( "update {$wpdb->prefix}serial_numbers set status='expired' where validity !='0' AND (order_date + INTERVAL validity DAY ) < NOW()" ); - } - - /** - * Send low stock email notification. - * - * @since 1.2.0 - * @return bool - */ - public function send_stock_alert_email() { - if ( 'yes' !== get_option( 'wc_serial_numbers_enable_stock_notification' ) ) { - return false; - } - - $stock_threshold = get_option( 'wc_serial_numbers_stock_threshold', 5 ); - $to = get_option( 'wc_serial_numbers_notification_recipient', get_option( 'admin_email' ) ); - if ( empty( $to ) ) { - return false; - } - - $low_stock_products = wcsn_get_stocks_count( $stock_threshold ); - if ( empty( $low_stock_products ) ) { - return false; - } - - $subject = __( 'Serial Numbers stock running low', 'wc-serial-numbers' ); - /** $woocommerce WooCommerce */ - global $woocommerce; - $mailer = $woocommerce->mailer(); - - ob_start(); - wcsn_get_template( 'email-stock-notification.php', array( 'low_stock_products' => $low_stock_products ) ); - $message = ob_get_contents(); - ob_get_clean(); - - $message = $mailer->wrap_message( $subject, $message ); - $headers = apply_filters( 'woocommerce_email_headers', '', 'wc_serial_numbers_low_stock_notification', $mailer ); - $mailer->send( $to, $subject, $message, $headers, array() ); - - exit(); - } -} diff --git a/includes/Frontend/Frontend.php b/includes/Frontend/Frontend.php deleted file mode 100644 index 50dc4d66..00000000 --- a/includes/Frontend/Frontend.php +++ /dev/null @@ -1,60 +0,0 @@ - - */ - public array $components = array( - Shortcodes::class, - ); - - /** - * Register hooks. - * - * @since 1.5.6 - * @return void - */ - public function register(): void { - add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); - add_action( 'wc_serial_numbers_before_display_order_keys', 'wcsn_display_order_keys_title', 10, 2 ); - add_action( 'wc_serial_numbers_display_order_keys', 'wcsn_display_order_keys_table', 10, 2 ); - } - - /** - * Enqueue frontend scripts. - * - * @since 1.5.6 - * @return void - */ - public function enqueue_scripts() { - $this->app->scripts->enqueue_style( 'wc-serial-numbers-frontend', 'frontend.css' ); - $this->app->scripts->enqueue_script( 'wc-serial-numbers-frontend', 'frontend.js', array( 'jquery' ) ); - wp_localize_script( - 'wc-serial-numbers-frontend', - 'wc_serial_numbers_frontend_vars', - array( - 'ajax_url' => admin_url( 'admin-ajax.php' ), - 'i18n' => array( - 'copied' => __( 'Copied', 'wc-serial-numbers' ), - 'loading' => __( 'Loading', 'wc-serial-numbers' ), - ), - ) - ); - } -} diff --git a/includes/Admin/Requests.php b/includes/Keys/Admin.php similarity index 90% rename from includes/Admin/Requests.php rename to includes/Keys/Admin.php index 13bcddf8..076c9def 100644 --- a/includes/Admin/Requests.php +++ b/includes/Keys/Admin.php @@ -1,6 +1,6 @@ exists() ) { + wp_safe_redirect( remove_query_arg( 'edit' ) ); + exit(); + } + } + + if ( $add ) { + $this->app->template->view( 'admin.edit-key', array( 'key' => new Key() ) ); + } elseif ( $edit ) { + $this->app->template->view( 'admin.edit-key', array( 'key' => $key ) ); + } else { + $this->app->template->view( 'admin.list-keys' ); + } + } + /** * Handle add/edit key. * diff --git a/includes/Actions.php b/includes/Keys/Keys.php similarity index 62% rename from includes/Actions.php rename to includes/Keys/Keys.php index 66106c4d..2257ce5a 100644 --- a/includes/Actions.php +++ b/includes/Keys/Keys.php @@ -1,22 +1,29 @@ + */ + public array $components = array( + Admin::class, + ); /** * Register hooks. @@ -30,8 +37,32 @@ public function register(): void { add_action( 'wc_serial_numbers_key_update_data', array( $this, 'encrypt_key' ) ); add_action( 'wc_serial_numbers_key_insert', array( $this, 'enable_product' ) ); add_action( 'wc_serial_numbers_key_deleted', array( $this, 'delete_activations' ) ); - add_action( 'wc_serial_numbers_activation_inserted', array( $this, 'update_activation_count' ) ); - add_action( 'wc_serial_numbers_activation_deleted', array( $this, 'update_activation_count' ) ); + add_action( 'wc_serial_numbers_key_saved', array( $this, 'clear_order_keys_cache' ) ); + add_action( 'wc_serial_numbers_key_deleted', array( $this, 'clear_order_keys_cache' ) ); + add_action( 'wc_serial_numbers_order_remove_keys', array( $this, 'clear_order_keys_cache' ) ); + add_action( 'wc_serial_numbers_order_add_keys', array( $this, 'clear_order_keys_cache' ) ); + add_action( 'wc_serial_numbers_hourly_event', array( $this, 'expire_outdated_serials' ) ); + } + + /** + * Clear order keys cache. + * + * @since 1.0.0 + * + * @return void + */ + public function clear_order_keys_cache() { + delete_transient( 'wcsn_products_stock_count' ); + } + + /** + * Disable all expired serial numbers + * + * since 1.0.0 + */ + public function expire_outdated_serials() { + global $wpdb; + $wpdb->query( "update {$wpdb->prefix}serial_numbers set status='expired' where validity !='0' AND (order_date + INTERVAL validity DAY ) < NOW()" ); } /** @@ -98,34 +129,4 @@ public function delete_activations( $key ) { } } } - - /** - * Revoke order item keys. - * - * @param bool $revoke The revoke flag. - * - * @since 1.4.6 - */ - public function revoke_order_item_keys( $revoke ) { - if ( 'yes' !== get_option( 'wc_serial_numbers_revoke_keys', 'yes' ) ) { - $revoke = false; - } - - return $revoke; - } - - - /** - * Update activation count. - * - * @param Activation $activation The activation object. - * - * @since 1.0.0 - */ - public function update_activation_count( $activation ) { - $key = Key::get( $activation->get_serial_id() ); - if ( $key ) { - $key->recount_remaining_activation(); - } - } } diff --git a/includes/Admin/ListTables/KeysTable.php b/includes/Keys/ListTable.php similarity index 99% rename from includes/Admin/ListTables/KeysTable.php rename to includes/Keys/ListTable.php index ccadb2e2..cb3ebcdd 100644 --- a/includes/Admin/ListTables/KeysTable.php +++ b/includes/Keys/ListTable.php @@ -1,6 +1,8 @@ + */ + public array $components = array( + Admin::class, + ); + /** * Register hooks. * diff --git a/includes/Plugin.php b/includes/Plugin.php index 8c13e9b6..7a6cbf44 100644 --- a/includes/Plugin.php +++ b/includes/Plugin.php @@ -20,15 +20,15 @@ class Plugin extends B8\App { */ protected array $components = array( Installer::class, - Cron::class, - Cache::class, Encryption::class, Cart::class, - Orders::class, - Stocks::class, - Actions::class, + Shop::class, + Keys\Keys::class, + Activations\Activations::class, + Orders\Orders::class, + Stocks\Stocks::class, + Products::class, RestAPI::class, - Frontend\Frontend::class, Compat::class, API::class, Admin\Admin::class, diff --git a/includes/Admin/Products.php b/includes/Products.php similarity index 97% rename from includes/Admin/Products.php rename to includes/Products.php index cb9d20c2..ae94e6b2 100644 --- a/includes/Admin/Products.php +++ b/includes/Products.php @@ -1,6 +1,6 @@ app->scripts->enqueue_style( 'wc-serial-numbers-frontend', 'frontend.css' ); + $this->app->scripts->enqueue_script( 'wc-serial-numbers-frontend', 'frontend.js', array( 'jquery' ) ); + wp_localize_script( + 'wc-serial-numbers-frontend', + 'wc_serial_numbers_frontend_vars', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'i18n' => array( + 'copied' => __( 'Copied', 'wc-serial-numbers' ), + 'loading' => __( 'Loading', 'wc-serial-numbers' ), + ), + ) + ); + } } diff --git a/includes/Admin/ListTables/StockTable.php b/includes/Stocks/ListTable.php similarity index 97% rename from includes/Admin/ListTables/StockTable.php rename to includes/Stocks/ListTable.php index 6e08b512..63e7f84f 100644 --- a/includes/Admin/ListTables/StockTable.php +++ b/includes/Stocks/ListTable.php @@ -1,6 +1,8 @@ set_stock_quantity( $quantity ); $product->save(); } + + /** + * Send low stock email notification. + * + * @since 1.2.0 + * @return bool + */ + public function send_stock_alert_email() { + if ( 'yes' !== get_option( 'wc_serial_numbers_enable_stock_notification' ) ) { + return false; + } + + $stock_threshold = get_option( 'wc_serial_numbers_stock_threshold', 5 ); + $to = get_option( 'wc_serial_numbers_notification_recipient', get_option( 'admin_email' ) ); + if ( empty( $to ) ) { + return false; + } + + $low_stock_products = wcsn_get_stocks_count( $stock_threshold ); + if ( empty( $low_stock_products ) ) { + return false; + } + + $subject = __( 'Serial Numbers stock running low', 'wc-serial-numbers' ); + /** $woocommerce WooCommerce */ + global $woocommerce; + $mailer = $woocommerce->mailer(); + + ob_start(); + wcsn_get_template( 'email-stock-notification.php', array( 'low_stock_products' => $low_stock_products ) ); + $message = ob_get_contents(); + ob_get_clean(); + + $message = $mailer->wrap_message( $subject, $message ); + $headers = apply_filters( 'woocommerce_email_headers', '', 'wc_serial_numbers_low_stock_notification', $mailer ); + $mailer->send( $to, $subject, $message, $headers, array() ); + + exit(); + } } diff --git a/includes/Admin/views/html-api-actions.php b/templates/admin/api-actions.php similarity index 100% rename from includes/Admin/views/html-api-actions.php rename to templates/admin/api-actions.php diff --git a/includes/Admin/views/html-api-validation.php b/templates/admin/api-validation.php similarity index 100% rename from includes/Admin/views/html-api-validation.php rename to templates/admin/api-validation.php diff --git a/includes/Admin/views/html-edit-key.php b/templates/admin/edit-key.php similarity index 100% rename from includes/Admin/views/html-edit-key.php rename to templates/admin/edit-key.php diff --git a/includes/Admin/views/html-list-activations.php b/templates/admin/list-activations.php similarity index 90% rename from includes/Admin/views/html-list-activations.php rename to templates/admin/list-activations.php index c4dcaaa0..61038afc 100644 --- a/includes/Admin/views/html-list-activations.php +++ b/templates/admin/list-activations.php @@ -8,7 +8,7 @@ defined( 'ABSPATH' ) || exit; -$list_table = new WooCommerceSerialNumbers\Admin\ListTables\ActivationsTable(); +$list_table = new WooCommerceSerialNumbers\Activations\ListTable(); $doaction = $list_table->current_action(); $list_table->process_bulk_actions( $doaction ); ?> diff --git a/includes/Admin/views/html-list-keys.php b/templates/admin/list-keys.php similarity index 95% rename from includes/Admin/views/html-list-keys.php rename to templates/admin/list-keys.php index eab9cdfd..981a856e 100644 --- a/includes/Admin/views/html-list-keys.php +++ b/templates/admin/list-keys.php @@ -8,7 +8,7 @@ defined( 'ABSPATH' ) || exit; -$list_table = new WooCommerceSerialNumbers\Admin\ListTables\KeysTable(); +$list_table = new WooCommerceSerialNumbers\Keys\ListTable(); $doaction = $list_table->current_action(); $list_table->process_bulk_actions( $doaction ); ?> diff --git a/includes/Admin/views/html-list-stock.php b/templates/admin/list-stock.php similarity index 85% rename from includes/Admin/views/html-list-stock.php rename to templates/admin/list-stock.php index 9b7cd740..13f25f1c 100644 --- a/includes/Admin/views/html-list-stock.php +++ b/templates/admin/list-stock.php @@ -6,7 +6,7 @@ */ defined( 'ABSPATH' ) || exit; -$list_table = new WooCommerceSerialNumbers\Admin\ListTables\StockTable(); +$list_table = new WooCommerceSerialNumbers\Stocks\ListTable(); ?>
diff --git a/includes/Admin/views/html-notice-dependency.php b/templates/admin/notices/dependency.php similarity index 100% rename from includes/Admin/views/html-notice-dependency.php rename to templates/admin/notices/dependency.php diff --git a/includes/Admin/views/html-reports.php b/templates/admin/reports.php similarity index 100% rename from includes/Admin/views/html-reports.php rename to templates/admin/reports.php diff --git a/includes/Admin/views/html-tools.php b/templates/admin/tools.php similarity index 100% rename from includes/Admin/views/html-tools.php rename to templates/admin/tools.php From 6cf7f453d80f64e2599e3c632f0f31856209ff10 Mon Sep 17 00:00:00 2001 From: sultann Date: Thu, 11 Jun 2026 01:27:10 +0600 Subject: [PATCH 2/5] Migrate Key and Activation models to the B8 models package with full legacy compatibility --- composer.json | 14 +- includes/Keys/Keys.php | 33 -- includes/Models/Activation.php | 102 ++++-- includes/Models/Key.php | 237 +++++++++---- includes/Models/Model.php | 619 ++++++++++++++++++++++++++++++++- 5 files changed, 863 insertions(+), 142 deletions(-) diff --git a/composer.json b/composer.json index b2da782c..e779e79f 100644 --- a/composer.json +++ b/composer.json @@ -4,12 +4,12 @@ "license": "GPL-2.0-or-later", "repositories": [ { "type": "vcs", "url": "git@github.com:byteever/plugin.git" }, - { "type": "vcs", "url": "git@github.com:pluginever/framework-model.git" } + { "type": "vcs", "url": "git@github.com:byteever/models.git" } ], "require": { "php": ">=7.4", - "byteever/plugin": "^1.0", - "pluginever/framework-model": "dev-trunk" + "byteever/models": "dev-trunk", + "byteever/plugin": "^1.0" }, "require-dev": { "byteever/byteever-sniffs": "^1.1.3", @@ -48,8 +48,10 @@ "strauss": [ "test -f /tmp/strauss.phar || curl -o /tmp/strauss.phar -L https://github.com/BrianHenryIE/strauss/releases/download/0.24.1/strauss.phar", "@php /tmp/strauss.phar", + "@patch-models", "@composer dump-autoload --no-scripts" - ] + ], + "patch-models": "php -r 'foreach (array_merge(glob(\"vendor/byteever/models/src/*.php\"), glob(\"vendor/byteever/models/src/Traits/*.php\"), glob(\"vendor/byteever/models/src/Relations/*.php\")) as $f) { $c = file_get_contents($f); $c = preg_replace(\"/->get\\\\((?!\\\\))/\", \"->get_attribute(\", $c); $c = str_replace(\"function get(string \", \"function get_attribute(string \", $c); file_put_contents($f, $c); }'" }, "extra": { "strauss": { @@ -57,8 +59,8 @@ "namespace_prefix": "WooCommerceSerialNumbers\\", "classmap_prefix": "WooCommerceSerialNumbers__", "packages": [ - "byteever/plugin", - "pluginever/framework-model" + "byteever/models", + "byteever/plugin" ] } } diff --git a/includes/Keys/Keys.php b/includes/Keys/Keys.php index 2257ce5a..27b5fd82 100644 --- a/includes/Keys/Keys.php +++ b/includes/Keys/Keys.php @@ -32,9 +32,6 @@ class Keys extends Component { * @return void */ public function register(): void { - add_action( 'wc_serial_numbers_key_db_data', array( $this, 'decrypt_key' ) ); - add_action( 'wc_serial_numbers_key_insert_data', array( $this, 'encrypt_key' ) ); - add_action( 'wc_serial_numbers_key_update_data', array( $this, 'encrypt_key' ) ); add_action( 'wc_serial_numbers_key_insert', array( $this, 'enable_product' ) ); add_action( 'wc_serial_numbers_key_deleted', array( $this, 'delete_activations' ) ); add_action( 'wc_serial_numbers_key_saved', array( $this, 'clear_order_keys_cache' ) ); @@ -65,36 +62,6 @@ public function expire_outdated_serials() { $wpdb->query( "update {$wpdb->prefix}serial_numbers set status='expired' where validity !='0' AND (order_date + INTERVAL validity DAY ) < NOW()" ); } - /** - * Decrypt key. - * - * @param array $data The key data. - * - * @since 1.4.6 - */ - public 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 function encrypt_key( $data ) { - if ( ! empty( $data['serial_key'] ) ) { - $data['serial_key'] = wcsn_encrypt_key( $data['serial_key'] ); - } - - return $data; - } - /** * Enable product. * diff --git a/includes/Models/Activation.php b/includes/Models/Activation.php index b4328b24..ad5aca98 100644 --- a/includes/Models/Activation.php +++ b/includes/Models/Activation.php @@ -12,14 +12,14 @@ */ class Activation extends Model { /** - * Table name. + * The table associated with the model. * * This is also used as table alias. * * @since 1.0.0 * @var string */ - protected $table_name = 'serial_numbers_activations'; + protected $table = 'serial_numbers_activations'; /** * Object type. @@ -30,20 +30,67 @@ class Activation extends Model { protected $object_type = 'activation'; /** - * Core data for this object. Name value pairs (name + default value). + * The cache group for the object type. + * + * @since 1.0.0 + * @var string + */ + protected $cache_group = 'serial_numbers_activations'; + + /** + * The table columns of the model. * * @since 1.0.0 * @var array */ - protected $core_data = array( + protected $columns = array( + 'id', + 'serial_id', + 'instance', + 'platform', + 'activation_time', + ); + + /** + * The model's attributes with their default values. + * + * @since 1.0.0 + * @var array + */ + protected $attributes = array( 'id' => 0, - 'serial_id' => '', + 'serial_id' => 0, 'instance' => '', 'platform' => '', 'activation_time' => '', // todo add ip address support. ); + /** + * The attributes that should be cast. + * + * @since 1.0.0 + * @var array + */ + protected $casts = array( + 'id' => 'int', + 'serial_id' => 'int', + ); + + /** + * The searchable attributes. + * + * @since 1.0.0 + * @var array + */ + protected $searchable = array( + 'id', + 'serial_id', + 'instance', + 'platform', + 'activation_time', + ); + /* |-------------------------------------------------------------------------- | Getters and Setters @@ -57,7 +104,7 @@ class Activation extends Model { * * @since 1.4.6 * - * @return string + * @return int */ public function get_id() { return $this->get_prop( 'id' ); @@ -94,7 +141,7 @@ public function get_serial_id( $context = 'view' ) { * * @since 1.4.6 * - * @return Key + * @return Key|null */ public function get_key() { if ( empty( $this->get_serial_id() ) ) { @@ -109,7 +156,7 @@ public function get_key() { * * @since 1.4.6 * - * @return int + * @return int|null */ public function get_product_id() { if ( empty( $this->get_key() ) ) { @@ -124,7 +171,7 @@ public function get_product_id() { * * @since 1.4.6 * - * @return string + * @return string|null */ public function get_product_title() { if ( empty( $this->get_key() ) ) { @@ -237,7 +284,7 @@ public function set_activation_time( $activation_time ) { * Saves an object in the database. * * @since 1.0.0 - * @return true|\WP_Error True on success, WP_Error on failure. + * @return static|\WP_Error Object instance on success, WP_Error on failure. */ public function save() { // Serial id is required. @@ -268,31 +315,32 @@ public function save() { */ /** - * Prepare where query. + * Translate legacy query arguments into the b8 query dialect. + * + * If order_id or product_id is given, the query is joined with the keys + * table and filtered by those columns. * - * @param array $clauses Query clauses. - * @param array $args Array of args to pass to the query method. + * @param array $args Query arguments. + * @param \WooCommerceSerialNumbers\B8\Models\Query $query Query object. * * @since 1.0.0 - * @return array + * @return array Translated query arguments. */ - protected function prepare_where_query( $clauses, $args = array() ) { - global $wpdb; - $clauses = parent::prepare_where_query( $clauses, $args ); - // If order_id or product_id is set, we need to join with the key table and filter by those. + protected function prepare_query_args( $args, $query ) { if ( ! empty( $args['order_id'] ) || ! empty( $args['product_id'] ) ) { - $key_table = ( new Key() )->get_table_name(); - $clauses['join'] .= " INNER JOIN {$wpdb->prefix}" . $key_table . " AS serial_numbers ON {$this->table_name}.serial_id = serial_numbers.id"; - } + $key_table = ( new Key() )->get_table_name(); + $query->join( $key_table, "{$this->get_table()}.serial_id", "{$key_table}.id" ); - if ( ! empty( $args['order_id'] ) ) { - $clauses['where'] .= $wpdb->prepare( ' AND serial_numbers.order_id = %d', $args['order_id'] ); - } + if ( ! empty( $args['order_id'] ) ) { + $query->where( "{$key_table}.order_id", absint( $args['order_id'] ) ); + } - if ( ! empty( $args['product_id'] ) ) { - $clauses['where'] .= $wpdb->prepare( ' AND serial_numbers.product_id = %d', $args['product_id'] ); + if ( ! empty( $args['product_id'] ) ) { + $query->where( "{$key_table}.product_id", absint( $args['product_id'] ) ); + } } + unset( $args['order_id'], $args['product_id'] ); - return $clauses; + return parent::prepare_query_args( $args, $query ); } } diff --git a/includes/Models/Key.php b/includes/Models/Key.php index fb86d722..f0837e82 100644 --- a/includes/Models/Key.php +++ b/includes/Models/Key.php @@ -12,12 +12,12 @@ */ class Key extends Model { /** - * Table name. + * The table associated with the model. * * @since 1.0.0 * @var string */ - protected $table_name = 'serial_numbers'; + protected $table = 'serial_numbers'; /** * Object type. @@ -28,12 +28,42 @@ class Key extends Model { protected $object_type = 'key'; /** - * Core data for this object. Name value pairs (name + default value). + * The cache group for the object type. + * + * @since 1.0.0 + * @var string + */ + protected $cache_group = 'serial_numbers'; + + /** + * The table columns of the model. * * @since 1.0.0 * @var array */ - protected $core_data = array( + protected $columns = array( + 'id', + 'serial_key', + 'product_id', + 'activation_limit', + 'activation_count', + 'order_id', + 'order_item_id', + 'vendor_id', + 'status', + 'validity', + 'order_date', + 'source', + 'created_date', + ); + + /** + * The model's attributes with their default values. + * + * @since 1.0.0 + * @var array + */ + protected $attributes = array( 'id' => 0, 'serial_key' => '', 'product_id' => 0, @@ -49,6 +79,45 @@ class Key extends Model { 'created_date' => '', ); + /** + * The attributes that should be cast. + * + * @since 1.0.0 + * @var array + */ + protected $casts = array( + 'id' => 'int', + 'product_id' => 'int', + 'activation_limit' => 'int', + 'activation_count' => 'int', + 'order_id' => 'int', + 'order_item_id' => 'int', + 'vendor_id' => 'int', + 'validity' => 'int', + ); + + /** + * The searchable attributes. + * + * @since 1.0.0 + * @var array + */ + protected $searchable = array( + 'id', + 'serial_key', + 'product_id', + 'activation_limit', + 'activation_count', + 'order_id', + 'order_item_id', + 'vendor_id', + 'status', + 'validity', + 'order_date', + 'source', + 'created_date', + ); + /* |-------------------------------------------------------------------------- | Getters and Setters @@ -62,7 +131,7 @@ class Key extends Model { * * @since 1.4.6 * - * @return string + * @return int */ public function get_id() { return $this->get_prop( 'id' ); @@ -501,7 +570,7 @@ public function set_created_date( $created_date ) { * Saves an object in the database. * * @since 1.0.0 - * @return true|\WP_Error True on success, WP_Error on failure. + * @return static|\WP_Error Object instance on success, WP_Error on failure. */ public function save() { // Product id is required. @@ -560,6 +629,43 @@ public function save() { return parent::save(); } + /** + * Create an item in the database. + * + * The serial key is encrypted at rest before it is written. + * + * @param array $data Data to be inserted. + * + * @since 1.0.0 + * @return \WP_Error|int The ID of the inserted item, or WP_Error on failure. + */ + protected function perform_create( $data ) { + if ( ! empty( $data['serial_key'] ) ) { + $data['serial_key'] = wcsn_encrypt_key( $data['serial_key'] ); + } + + return parent::perform_create( $data ); + } + + /** + * Update an item in the database. + * + * The serial key is encrypted at rest before it is written. + * + * @param int $id ID of the item to be updated. + * @param array $data Data to be updated. + * + * @since 1.0.0 + * @return \WP_Error|bool True on success, or WP_Error on failure. + */ + protected function perform_update( $id, $data ) { + if ( ! empty( $data['serial_key'] ) ) { + $data['serial_key'] = wcsn_encrypt_key( $data['serial_key'] ); + } + + return parent::perform_update( $id, $data ); + } + /* |-------------------------------------------------------------------------- | Query Methods @@ -572,7 +678,7 @@ public function save() { /** * Retrieve the object instance. * - * @param int|array|static $data Object ID or array of arguments. + * @param int|array|static $data Object ID or array of column conditions. * * @since 1.0.0 * @@ -588,62 +694,37 @@ public static function get( $data ) { } /** - * Prepare where query. + * Translate legacy query arguments into the b8 query dialect. * - * @param array $clauses Query clauses. - * @param array $args Array of args to pass to the query method. + * Handles the customer_id constraint and the search clause, which must + * match the encrypted serial key at rest. + * + * @param array $args Query arguments. + * @param \WooCommerceSerialNumbers\B8\Models\Query $query Query object. * * @since 1.0.0 - * @return array + * @return array Translated query arguments. */ - protected function prepare_where_query( $clauses, $args = array() ) { - global $wpdb; - $clauses = parent::prepare_where_query( $clauses, $args ); - + protected function prepare_query_args( $args, $query ) { // If customer id is set, find the orders having that customer id and limit the results to those orders. if ( ! empty( $args['customer_id'] ) ) { - $customer_id = absint( $args['customer_id'] ); - $order_ids = wc_get_orders( + $order_ids = wc_get_orders( array( - 'customer_id' => $customer_id, + 'customer_id' => absint( $args['customer_id'] ), 'limit' => - 1, 'return' => 'ids', ) ); if ( ! empty( $order_ids ) ) { - $clauses['where'] .= " AND {$this->table_name}.order_id IN (" . implode( ',', $order_ids ) . ')'; + $query->where( 'order_id', 'IN', $order_ids ); } else { - $clauses['where'] .= ' AND 0'; + $query->where( 'id', '=', 0 ); } } + unset( $args['customer_id'] ); - return $clauses; - } - - /** - * Prepare search query. - * - * @param array $clauses Query clauses. - * @param array $args Array of args to pass to the query method. - * - * @since 1.0.0 - * @return array - */ - protected function prepare_search_query( $clauses, $args = array() ) { - global $wpdb; - /** - * Filter the search query before setting up the query. - * - * @param array $clauses Query clauses. - * @param array $args Query arguments. - * @param static $this Current instance of the class. - * - * @return array - * @since 1.0.0 - */ - $clauses = apply_filters( $this->get_hook_prefix() . '_pre_setup_search_query', $clauses, $args, $this ); - + // Search must compare the serial key column against the encrypted value. if ( ! empty( $args['search'] ) ) { $search = $args['search']; if ( ! empty( $args['search_columns'] ) ) { @@ -652,42 +733,52 @@ protected function prepare_search_query( $clauses, $args = array() ) { /** * Filter the columns to search in when performing a search query. * - * @param array $search_columns Array of columns to search in. - * @param array $args Query arguments. - * @param static $object Current instance of the class. + * @param array $search_columns Array of columns to search in. + * @param array $args Query arguments. + * @param static $item Current instance of the class. * - * @return array * @since 1.0.0 */ - $search_columns = apply_filters( $this->get_hook_prefix() . '_search_columns', $this->get_searchable_keys(), $args, $this ); + $search_columns = apply_filters( $this->get_hook_prefix() . '_search_columns', $this->get_searchable(), $args, $this ); } $search_columns = array_filter( array_unique( $search_columns ) ); - $like = '%' . $wpdb->esc_like( $search ) . '%'; - - $search_clauses = array(); - foreach ( $search_columns as $column ) { - if ( 'serial_key' === $column ) { - $like = '%' . $wpdb->esc_like( wcsn_encrypt_key( $search ) ) . '%'; - } - $search_clauses[] = $wpdb->prepare( $this->table_name . '.' . $column . ' LIKE %s', $like ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - } - if ( ! empty( $search_clauses ) ) { - $clauses['where'] .= 'AND (' . implode( ' OR ', $search_clauses ) . ')'; + if ( ! empty( $search_columns ) ) { + $query->where( + function ( $q ) use ( $search_columns, $search ) { + foreach ( $search_columns as $column ) { + $term = 'serial_key' === $column ? wcsn_encrypt_key( $search ) : $search; + $q->where( $column, 'LIKE', $term ); + } + }, + 'OR' + ); } + + unset( $args['search'], $args['search_columns'] ); + } + + return parent::prepare_query_args( $args, $query ); + } + + /** + * Prepare data read from the database before it is hydrated. + * + * The serial key is stored encrypted and decrypted on read. + * + * @param array $data Data read from the database. + * + * @since 1.0.0 + * @return array Prepared data. + */ + protected function prepare_db_data( $data ) { + $data = parent::prepare_db_data( $data ); + + if ( ! empty( $data['serial_key'] ) ) { + $data['serial_key'] = wcsn_decrypt_key( $data['serial_key'] ); } - /** - * Filter the search query after setting up the query. - * - * @param array $clauses Query clauses. - * @param array $args Query arguments. - * @param static $this Current instance of the class. - * - * @return array - * @since 1.0.0 - */ - return apply_filters( $this->get_hook_prefix() . '_setup_search_query', $clauses, $args, $this ); + return $data; } /* diff --git a/includes/Models/Model.php b/includes/Models/Model.php index 5d1bcf9a..578c8c98 100644 --- a/includes/Models/Model.php +++ b/includes/Models/Model.php @@ -7,15 +7,628 @@ /** * Class Model. * + * Base model built on the b8 models package. It also bridges the legacy + * framework-model API (getters/setters, query dialect and hooks) so existing + * call sites and extensions keep working unchanged. + * * @since 1.0.0 * @package WooCommerceSerialNumbers\Models */ -class Model extends \WooCommerceSerialNumbers\Lib\Model { +abstract class Model extends \WooCommerceSerialNumbers\B8\Models\Model { + /** - * Hook prefix. + * Hook prefix for the model. + * + * Hooks are fired as "{hook_prefix}_{object_type}_{hook}", which resolves + * to the legacy "wc_serial_numbers_key_*" and "wc_serial_numbers_activation_*" names. * * @since 1.0.0 * @var string */ - protected $hook_prefix = 'wc_serial_numbers_'; + protected $hook_prefix = 'wc_serial_numbers'; + + /** + * Default query variables. + * + * Matches the legacy query defaults (order by id ASC, 20 per page). + * + * @since 1.0.0 + * @var array + */ + protected $query_vars = array( + 'orderby' => 'id', + 'order' => 'ASC', + 'limit' => 20, + ); + + /* + |-------------------------------------------------------------------------- + | Static Methods + |-------------------------------------------------------------------------- + | Legacy static entry points kept for backwards compatibility. + */ + + /** + * Retrieve the object instance. + * + * @param int|array|object|static $data Object ID, array of column conditions, or object. + * + * @since 1.0.0 + * @return static|false Object instance on success, false on failure. + */ + public static function get( $data ) { + if ( $data instanceof static ) { + $data = $data->get_key_value(); + } elseif ( is_object( $data ) ) { + $vars = get_object_vars( $data ); + $pk = ( new static() )->get_primary_key(); + $data = ! empty( $vars[ $pk ] ) ? $vars[ $pk ] : null; + } + + $item = static::find( $data ); + + return $item ? $item : false; + } + + /** + * Insert or update an object in the database. + * + * Accepts both the legacy ( $data, $wp_error ) and the b8 + * ( $data, $search, $wp_error ) call signatures. + * + * @param array|object $data Model data to insert or update. + * @param array|string|bool|null $search Search column(s), or the legacy $wp_error flag. + * @param bool $wp_error Whether to return a WP_Error on failure. + * + * @since 1.0.0 + * @return static|false|\WP_Error Object instance on success, false or WP_Error on failure. + */ + public static function insert( $data, $search = null, $wp_error = true ) { + // Legacy signature support: insert( $data, $wp_error ). + if ( is_bool( $search ) ) { + $wp_error = $search; + $search = null; + } + + return parent::insert( $data, $search, $wp_error ); + } + + /* + |-------------------------------------------------------------------------- + | CRUD Methods + |-------------------------------------------------------------------------- + */ + + /** + * Saves the object in the database. + * + * @since 1.0.0 + * @return static|\WP_Error Object instance on success, WP_Error on failure. + */ + public function save() { + /** + * Filters whether an item contains valid data before saving. + * + * Returning a WP_Error prevents the item from being saved. + * + * @param null|\WP_Error $check Null to allow saving, WP_Error to block. + * @param static $item Model object. + * + * @since 1.0.0 + */ + $check = apply_filters( $this->get_hook_prefix() . '_sanitize_data', null, $this ); + if ( is_wp_error( $check ) ) { + return $check; + } + + return parent::save(); + } + + /** + * Deletes the object from the database. + * + * @since 1.0.0 + * @return bool|mixed|\WP_Error True on success, false when the object does not exist. + */ + public function delete() { + if ( ! $this->exists() ) { + return false; + } + + /** + * Filters whether an item delete should take place. + * + * @param null|mixed $check Null to proceed with the deletion, any other value to short-circuit. + * @param static $item Model object. + * @param array $data Model data array. + * + * @since 1.0.0 + */ + $check = apply_filters( $this->get_hook_prefix() . '_check_delete', null, $this, $this->to_array() ); + if ( null !== $check ) { + return $check; + } + + return parent::delete(); + } + + /** + * Create an item in the database. + * + * @param array $data Data to be inserted. + * + * @since 1.0.0 + * @return \WP_Error|int The ID of the inserted item, or WP_Error on failure. + */ + protected function perform_create( $data ) { + /** + * Filters the data to be inserted into the database. + * + * @param array $data Data to be inserted. + * @param static $item Model object. + * + * @since 1.0.0 + */ + $data = apply_filters( $this->get_hook_prefix() . '_insert_data', $data, $this ); + + return parent::perform_create( $data ); + } + + /** + * Update an item in the database. + * + * @param int $id ID of the item to be updated. + * @param array $data Data to be updated. + * + * @since 1.0.0 + * @return \WP_Error|bool True on success, or WP_Error on failure. + */ + protected function perform_update( $id, $data ) { + /** + * Filters the data to be updated in the database. + * + * @param array $data Data to be updated. + * @param static $item Model object. + * + * @since 1.0.0 + */ + $data = apply_filters( $this->get_hook_prefix() . '_update_data', $data, $this ); + + return parent::perform_update( $id, $data ); + } + + /* + |-------------------------------------------------------------------------- + | Query Methods + |-------------------------------------------------------------------------- + */ + + /** + * Get the query object for the model, or run a query when args are given. + * + * @param array|null $args Optional. The query arguments. + * + * @since 1.0.0 + * @return \WooCommerceSerialNumbers\B8\Models\Query|array|int Query object, or the items/count when args are given. + */ + public function new_query( $args = null ) { + if ( is_null( $args ) ) { + return parent::new_query(); + } + + $args = (array) $args; + $output = isset( $args['output'] ) ? $args['output'] : null; + unset( $args['output'] ); + + $items = parent::new_query( $args ); + + // Legacy 'output' argument support. + if ( $output && is_array( $items ) && in_array( $output, array( ARRAY_A, ARRAY_N, OBJECT ), true ) ) { + foreach ( $items as $index => $item ) { + if ( ! $item instanceof self ) { + continue; + } + if ( ARRAY_A === $output ) { + $items[ $index ] = $item->to_array(); + } elseif ( ARRAY_N === $output ) { + $items[ $index ] = array_values( $item->to_array() ); + } else { + $items[ $index ] = (object) $item->to_array(); + } + } + } + + return $items; + } + + /** + * Apply filters with the automatic hook prefix. + * + * Intercepts the read and query extension points so legacy behavior + * (query-arg dialect, db data filters) applies on every query path. + * + * @param string $hook The filter name. + * @param mixed $value The value to filter. + * @param mixed ...$args Additional arguments. + * + * @since 1.0.0 + * @return mixed The filtered value. + */ + public function apply_filters( string $hook, $value, ...$args ) { + if ( 'query_args' === $hook && isset( $args[0] ) ) { + $value = $this->prepare_query_args( (array) $value, $args[0] ); + } elseif ( 'data' === $hook && is_array( $value ) ) { + $value = $this->prepare_db_data( $value ); + } + + return parent::apply_filters( $hook, $value, ...$args ); + } + + /** + * Translate legacy query arguments into the b8 query dialect. + * + * @param array $args Query arguments. + * @param \WooCommerceSerialNumbers\B8\Models\Query $query Query object. + * + * @since 1.0.0 + * @return array Translated query arguments. + */ + protected function prepare_query_args( $args, $query ) { + // 'paged' is the legacy name of 'page'. + if ( isset( $args['paged'] ) ) { + if ( empty( $args['page'] ) ) { + $args['page'] = max( 1, (int) $args['paged'] ); + } + unset( $args['paged'] ); + } + + // 'nopaging' and non-positive 'per_page'/'limit' disable the LIMIT clause. + if ( ! empty( $args['nopaging'] ) ) { + $args['limit'] = -1; + } + unset( $args['nopaging'] ); + if ( isset( $args['per_page'] ) && (int) $args['per_page'] < 1 ) { + $args['limit'] = -1; + unset( $args['per_page'] ); + } + if ( isset( $args['limit'] ) && (int) $args['limit'] < 1 ) { + $args['limit'] = -1; + } + + // 'fields' is the legacy name of 'select'. + if ( isset( $args['fields'] ) ) { + $fields = is_array( $args['fields'] ) ? $args['fields'] : preg_split( '/[,\s]+/', (string) $args['fields'] ); + if ( in_array( 'ids', (array) $fields, true ) ) { + $args['select'] = 'ids'; + } + unset( $args['fields'] ); + } + + // Found rows are now only calculated on demand. + unset( $args['no_count'] ); + + // Legacy multi-column 'orderby' values. + if ( ! empty( $args['orderby'] ) && is_string( $args['orderby'] ) && preg_match( '/[\s,]/', trim( $args['orderby'] ) ) ) { + $orderby = array(); + foreach ( array_filter( array_map( 'trim', explode( ',', $args['orderby'] ) ) ) as $part ) { + $part = preg_split( '/\s+/', $part ); + $orderby[] = array( $part[0] => isset( $part[1] ) ? $part[1] : '' ); + } + $args['orderby'] = array_merge( ...$orderby ); + } + if ( ! empty( $args['orderby'] ) && is_array( $args['orderby'] ) ) { + $order = isset( $args['order'] ) && is_string( $args['order'] ) ? strtoupper( $args['order'] ) : 'ASC'; + $orderby = array(); + foreach ( $args['orderby'] as $column => $direction ) { + if ( is_numeric( $column ) ) { + $column = $direction; + $direction = $order; + } + $direction = in_array( strtoupper( (string) $direction ), array( 'ASC', 'DESC' ), true ) ? strtoupper( $direction ) : $order; + $orderby[] = array( + 'column' => $column, + 'direction' => $direction, + ); + } + $args['orderby'] = $orderby; + } + + // Legacy 'where_query' clauses. + if ( ! empty( $args['where_query'] ) && is_array( $args['where_query'] ) ) { + foreach ( $args['where_query'] as $index => $clause ) { + if ( ! is_numeric( $index ) && is_string( $clause ) ) { + $clause = array( + 'column' => $index, + 'value' => $clause, + 'compare' => '=', + ); + } + $clause = wp_parse_args( + (array) $clause, + array( + 'column' => '', + 'value' => '', + 'compare' => '=', + ) + ); + if ( empty( $clause['column'] ) ) { + continue; + } + $query->where( $clause['column'], strtoupper( $clause['compare'] ), $clause['value'] ); + } + } + unset( $args['where_query'] ); + + return $args; + } + + /** + * Prepare data read from the database before it is hydrated. + * + * @param array $data Data read from the database. + * + * @since 1.0.0 + * @return array Prepared data. + */ + protected function prepare_db_data( $data ) { + /** + * Filters the data retrieved from the database. + * + * @param array $data Data retrieved from the database. + * @param static $item Model object. + * + * @since 1.0.0 + */ + return apply_filters( $this->get_hook_prefix() . '_db_data', $data, $this ); + } + + /* + |-------------------------------------------------------------------------- + | Getters and Setters + |-------------------------------------------------------------------------- + | Legacy prop accessors used by the concrete models and external code. + */ + + /** + * Get an attribute, routed through the model's getter method when one exists. + * + * @param string $key The name of the attribute. + * @param mixed $fallback Optional fallback value if the key is not found. + * + * @since 1.0.0 + * @return mixed The value of the attribute. + */ + public function get_attribute( string $key, $fallback = null ) { + $getter = "get_{$key}"; + if ( ! method_exists( self::class, $getter ) && method_exists( $this, $getter ) ) { + return $this->$getter(); + } + + return parent::get_attribute( $key, $fallback ); + } + + /** + * Set an attribute, routed through the model's setter method when one exists. + * + * @param string $key The name of the attribute. + * @param mixed $value The value to set. + * + * @since 1.0.0 + * @return static + */ + public function set( string $key, $value ) { + $setter = "set_{$key}"; + if ( ! method_exists( self::class, $setter ) && method_exists( $this, $setter ) ) { + $this->$setter( $value ); + + return $this; + } + + return parent::set( $key, $value ); + } + + /** + * Gets a prop for a getter method. + * + * @param string $prop Name of prop to get. + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * + * @since 1.0.0 + * @return mixed + */ + protected function get_prop( $prop, $context = 'edit' ) { + $value = null; + if ( array_key_exists( $prop, $this->attributes ) ) { + $value = $this->attributes[ $prop ]; + } elseif ( array_key_exists( $prop, $this->metadata ) ) { + $value = $this->metadata[ $prop ]; + } + + if ( 'view' === $context ) { + /** + * Filters the prop value when read for viewing. + * + * @param mixed $value Prop value. + * @param static $item Model object. + * + * @since 1.0.0 + */ + $value = apply_filters( $this->get_hook_prefix() . '_get_' . $prop, $value, $this ); + } + + return $value; + } + + /** + * Sets a prop for a setter method. + * + * Only known columns and registered metadata are written, matching the + * legacy behavior of ignoring unknown props. + * + * @param string $prop Name of prop to set. + * @param mixed $value Value to set. + * + * @since 1.0.0 + * @return void + */ + protected function set_prop( $prop, $value ) { + if ( array_key_exists( $prop, $this->attributes ) ) { + $this->attributes[ $prop ] = $value; + } elseif ( array_key_exists( $prop, $this->metadata ) ) { + $this->metadata[ $prop ] = $value; + } + } + + /** + * Set the model's data. + * + * @param array|object $props Array or object of properties to set. + * + * @since 1.0.0 + * @return static + */ + public function set_data( $props ) { + return $this->fill( $props ); + } + + /** + * Get the model's data. + * + * @since 1.0.0 + * @return array + */ + public function get_data() { + return $this->to_array(); + } + + /** + * Get date prop. + * + * @param string $prop Name of prop to get. + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @param string $format Date format. + * + * @since 1.0.0 + * @return string|null + */ + public function get_date_prop( $prop, $context = 'edit', $format = 'Y-m-d H:i:s' ) { + $datetime = $this->sanitize_date( $this->get_prop( $prop ) ); + + $value = $datetime ? date( $format, strtotime( $datetime ) ) : null; // @codingStandardsIgnoreLine - date() is ok here. + + if ( 'view' === $context ) { + /** + * Filters the date prop value when read for viewing. + * + * @param mixed $value Prop value. + * @param static $item Model object. + * + * @since 1.0.0 + */ + $value = apply_filters( $this->get_hook_prefix() . '_get_' . $prop, $value, $this ); + } + + return $value; + } + + /** + * Sets a date prop whilst handling formatting and datetime objects. + * + * @param string $prop Name of prop to set. + * @param string|integer $value Value of the prop. + * @param string $format Date format. + * + * @since 1.0.0 + * @return void + */ + public function set_date_prop( $prop, $value, $format = 'Y-m-d H:i:s' ) { + $date = $this->sanitize_date( $value ); + if ( ! empty( $date ) ) { + $date = date( $format, strtotime( $date ) ); // @codingStandardsIgnoreLine - date() is ok here. + } + $this->set_prop( $prop, $date ); + } + + /* + |-------------------------------------------------------------------------- + | Helpers + |-------------------------------------------------------------------------- + */ + + /** + * Get the hook prefix including the object type. + * + * Kept for backwards compatibility with the legacy model API, where the + * prefix included the object type (e.g. "wc_serial_numbers_key"). + * + * @since 1.0.0 + * @return string + */ + public function get_hook_prefix(): string { + return $this->hook_prefix . '_' . $this->object_type; + } + + /** + * Get table name. + * + * @since 1.0.0 + * @return string + */ + public function get_table_name() { + return $this->get_table(); + } + + /** + * Checks if a date is valid or not. + * + * @param string $date Date to check. + * + * @since 1.0.0 + * @return bool + */ + public function is_date_valid( $date ) { + if ( empty( preg_replace( '/[^0-9]/', '', (string) $date ) ) ) { + return false; + } + + return (bool) strtotime( (string) $date ); + } + + /** + * Sanitize date property. + * If the date is a valid date, it will be returned to the given format. + * + * @param string $date Date. + * + * @since 1.0.0 + * @return string|null + */ + public function sanitize_date( $date ) { + if ( empty( $date ) || '0000-00-00 00:00:00' === $date || '0000-00-00' === $date ) { + return null; + } + + if ( ! $this->is_date_valid( $date ) ) { + return null; + } + + // get the date format from the given date. + $length = strlen( (string) $date ); + switch ( $length ) { + case 8: + $format = 'H:i:s'; + break; + case 10: + $format = 'Y-m-d'; + break; + case 19: + default: + $format = 'Y-m-d H:i:s'; + break; + } + + $d = \DateTime::createFromFormat( $format, (string) $date ); + + return $d && $d->format( $format ) === $date ? $d->format( $format ) : null; + } } From 706956008337195a1f626d244c5b206d84b78916 Mon Sep 17 00:00:00 2001 From: sultann Date: Thu, 11 Jun 2026 01:27:27 +0600 Subject: [PATCH 3/5] Add full REST API with keys and activations resources following the Routes and Controllers pattern --- includes/Plugin.php | 2 +- includes/RestAPI/Controllers/Activations.php | 378 +++++++++++++++++ includes/RestAPI/Controllers/Controller.php | 290 +++++++++++++ includes/RestAPI/Controllers/Keys.php | 382 ++++++++++++++++++ .../Controllers/Software.php} | 25 +- includes/RestAPI/Routes.php | 47 +++ includes/functions.php | 2 +- .../{Functions/Template.php => template.php} | 0 wc-serial-numbers.php | 2 + 9 files changed, 1109 insertions(+), 19 deletions(-) create mode 100644 includes/RestAPI/Controllers/Activations.php create mode 100644 includes/RestAPI/Controllers/Controller.php create mode 100644 includes/RestAPI/Controllers/Keys.php rename includes/{RestAPI.php => RestAPI/Controllers/Software.php} (96%) create mode 100644 includes/RestAPI/Routes.php rename includes/{Functions/Template.php => template.php} (100%) diff --git a/includes/Plugin.php b/includes/Plugin.php index 7a6cbf44..e85f5e22 100644 --- a/includes/Plugin.php +++ b/includes/Plugin.php @@ -28,7 +28,7 @@ class Plugin extends B8\App { Orders\Orders::class, Stocks\Stocks::class, Products::class, - RestAPI::class, + RestAPI\Routes::class, Compat::class, API::class, Admin\Admin::class, diff --git a/includes/RestAPI/Controllers/Activations.php b/includes/RestAPI/Controllers/Activations.php new file mode 100644 index 00000000..d5262c8a --- /dev/null +++ b/includes/RestAPI/Controllers/Activations.php @@ -0,0 +1,378 @@ + rest_authorization_required_code() ) ); + } + + $query = Activation::query(); + + foreach ( $request->get_params() as $key => $value ) { + $query->set_var( $key, $value ); + } + + $query = $this->apply_query_filters( $query, $request, 'activations' ); + + /** + * Filters the activation query before execution. + * + * @since 2.4.0 + * + * @param mixed $query Query object. + * @param WP_REST_Request $request Request object. + */ + $query = WCSN()->apply_filters( 'rest_activation_query', $query, $request ); + + $items = $query->get(); + $total = $query->count(); + $page = (int) $query->get_var( 'page', 1 ); + $per_page = (int) $query->get_var( 'per_page', 20 ); + + $results = array(); + foreach ( $items as $item ) { + $data = $this->prepare_item_for_response( $item, $request ); + $results[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $results ); + $max_pages = $total > 0 ? (int) ceil( $total / $per_page ) : 0; + + $response->header( 'X-WP-Total', (int) $total ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = add_query_arg( urlencode_deep( $request->get_query_params() ), rest_url( $request->get_route() ) ); + + if ( $page > 1 ) { + $prev_page = min( $page - 1, $max_pages ); + $response->link_header( 'prev', add_query_arg( 'page', $prev_page, $base ) ); + } + + if ( $max_pages > $page ) { + $response->link_header( 'next', add_query_arg( 'page', $page + 1, $base ) ); + } + + return $response; + } + + /** + * Retrieves one activation. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + public function get_item( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce capability. + return new WP_Error( 'rest_forbidden', __( 'Not allowed.', 'wc-serial-numbers' ), array( 'status' => rest_authorization_required_code() ) ); + } + + $item = Activation::find( $request->get_param( 'id' ) ); + + if ( ! $item ) { + return new WP_Error( 'rest_activation_not_found', __( 'Activation not found.', 'wc-serial-numbers' ), array( 'status' => 404 ) ); + } + + return $this->prepare_item_for_response( $item, $request ); + } + + /** + * Creates one activation. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + public function create_item( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce capability. + return new WP_Error( 'rest_forbidden', __( 'Not allowed.', 'wc-serial-numbers' ), array( 'status' => rest_authorization_required_code() ) ); + } + + if ( ! empty( $request->get_param( 'id' ) ) ) { + return new WP_Error( 'rest_activation_exists', __( 'Cannot create existing activation.', 'wc-serial-numbers' ), array( 'status' => 400 ) ); + } + + $item = $this->prepare_item_for_database( $request ); + + if ( is_wp_error( $item ) ) { + return $item; + } + + /** + * Filters the activation object before saving. + * + * @since 2.4.0 + * + * @param Activation $item Activation object. + * @param WP_REST_Request $request Request object. + */ + $item = WCSN()->apply_filters( 'rest_pre_insert_activation', $item, $request ); + $item = $item->save(); + + if ( is_wp_error( $item ) ) { + return $item; + } + + $response = $this->prepare_item_for_response( $item, $request ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( $request->get_route() . '/' . $item->id ) ); + + return $response; + } + + /** + * Updates one activation. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + public function update_item( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce capability. + return new WP_Error( 'rest_forbidden', __( 'Not allowed.', 'wc-serial-numbers' ), array( 'status' => rest_authorization_required_code() ) ); + } + + $item = $this->prepare_item_for_database( $request ); + + if ( is_wp_error( $item ) ) { + return $item; + } + + if ( ! $item->exists() ) { + return new WP_Error( 'rest_activation_not_found', __( 'Activation not found.', 'wc-serial-numbers' ), array( 'status' => 404 ) ); + } + + /** + * Filters the activation object before saving. + * + * @since 2.4.0 + * + * @param Activation $item Activation object. + * @param WP_REST_Request $request Request object. + */ + $item = WCSN()->apply_filters( 'rest_pre_insert_activation', $item, $request ); + $item = $item->save(); + + if ( is_wp_error( $item ) ) { + return $item; + } + + return $this->prepare_item_for_response( $item, $request ); + } + + /** + * Deletes one activation. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + public function delete_item( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce capability. + return new WP_Error( 'rest_forbidden', __( 'Not allowed.', 'wc-serial-numbers' ), array( 'status' => rest_authorization_required_code() ) ); + } + + $item = Activation::find( $request->get_param( 'id' ) ); + + if ( ! $item ) { + return new WP_Error( 'rest_activation_not_found', __( 'Activation not found.', 'wc-serial-numbers' ), array( 'status' => 404 ) ); + } + + $response = $this->prepare_item_for_response( $item, $request ); + $previous = $response->get_data(); + $deleted = $item->delete(); + + if ( is_wp_error( $deleted ) ) { + return $deleted; + } + + return rest_ensure_response( + array( + 'deleted' => true, + 'previous' => $previous, + ) + ); + } + + /** + * Retrieves form options for activations. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + public function get_options( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce capability. + return new WP_Error( 'rest_forbidden', __( 'Not allowed.', 'wc-serial-numbers' ), array( 'status' => rest_authorization_required_code() ) ); + } + + $options = array( + 'bulk_actions' => array( + array( + 'value' => 'delete', + 'label' => __( 'Delete', 'wc-serial-numbers' ), + ), + ), + 'import_fields' => array(), + 'export_columns' => array(), + ); + + /** + * Filters the activation form options. + * + * @since 2.4.0 + * + * @param array $options Options data. + * @param WP_REST_Request $request Request object. + */ + return rest_ensure_response( WCSN()->apply_filters( 'rest_activation_options', $options, $request ) ); + } + + /** + * Batch operations. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + public function batch_items( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce capability. + return new WP_Error( 'rest_forbidden', __( 'Not allowed.', 'wc-serial-numbers' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return $this->process_batch( $request ); + } + + /** + * Import activations from CSV. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + public function import_items( $request ) { + return new WP_Error( 'rest_not_implemented', __( 'Import not yet implemented.', 'wc-serial-numbers' ), array( 'status' => 501 ) ); + } + + /** + * Export activations to CSV. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + public function export_items( $request ) { + return new WP_Error( 'rest_not_implemented', __( 'Export not yet implemented.', 'wc-serial-numbers' ), array( 'status' => 501 ) ); + } + + /** + * Prepare activation for response. + * + * @since 2.4.0 + * + * @param Activation $item Activation object. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response Response. + */ + public function prepare_item_for_response( $item, $request ): WP_REST_Response { + $raw = $item->to_array(); + $data = array(); + + foreach ( $raw as $key => $value ) { + switch ( $key ) { + case 'activation_time': + $data[ $key ] = $this->prepare_date_response( $value ); + break; + + default: + $data[ $key ] = $value; + break; + } + } + + /** + * Filters the activation data before creating the response. + * + * @since 2.4.0 + * + * @param WP_REST_Response $response Response object. + * @param Activation $item Activation object. + * @param WP_REST_Request $request Request object. + */ + return WCSN()->apply_filters( 'rest_prepare_activation', rest_ensure_response( $data ), $item, $request ); + } + + /** + * Prepare activation for database. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return Activation|WP_Error Activation object or error. + */ + protected function prepare_item_for_database( $request ) { + $id = $request->get_param( 'id' ); + $item = $id ? Activation::find( $id ) : new Activation(); + + if ( $id && ! $item ) { + return new WP_Error( 'rest_activation_not_found', __( 'Activation not found.', 'wc-serial-numbers' ), array( 'status' => 404 ) ); + } + + foreach ( $request->get_params() as $key => $value ) { + switch ( $key ) { + case 'activation_time': + $item->$key = $this->prepare_date_for_database( $value ); + break; + + default: + $item->$key = $value; + break; + } + } + + return $item; + } +} diff --git a/includes/RestAPI/Controllers/Controller.php b/includes/RestAPI/Controllers/Controller.php new file mode 100644 index 00000000..e0a758b0 --- /dev/null +++ b/includes/RestAPI/Controllers/Controller.php @@ -0,0 +1,290 @@ + 'http://json-schema.org/draft-04/schema#', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + } + + /** + * Retrieves the query params for the collection. + * + * @since 2.4.0 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['per_page']['default'] = 20; + + return $params; + } + + /** + * Prepare date for response (MySQL to RFC3339). + * + * @since 2.4.0 + * + * @param string|null $date MySQL datetime. + * + * @return string|null RFC3339 date. + */ + protected function prepare_date_response( ?string $date ): ?string { + if ( empty( $date ) || '0000-00-00 00:00:00' === $date ) { + return null; + } + + return mysql_to_rfc3339( $date ); + } + + /** + * Prepare date for database (RFC3339 to MySQL). + * + * @since 2.4.0 + * + * @param string|null $date RFC3339 datetime. + * + * @return string|null MySQL datetime. + */ + protected function prepare_date_for_database( ?string $date ): ?string { + if ( empty( $date ) ) { + return null; + } + + $timestamp = rest_parse_date( $date ); + + if ( ! $timestamp ) { + return null; + } + + return gmdate( 'Y-m-d H:i:s', $timestamp ); + } + + /** + * Convert items into value/label options array. + * + * Accepts two formats: + * 1. Associative array (key => label): prepare_options( ['available' => 'Available', 'sold' => 'Sold'] ) + * 2. Array of arrays/objects with pluck keys: prepare_options( $items, 'id', 'name' ) + * + * @since 2.4.0 + * + * @param array $items Items to convert. + * @param string $value_key Key to pluck for value. Empty = treat as associative key => label. + * @param string $label_key Key to pluck for label. + * + * @return array Options array of [ ['value' => ..., 'label' => ...], ... ]. + */ + protected function prepare_options( array $items, string $value_key = '', string $label_key = '' ): array { + if ( empty( $items ) ) { + return array(); + } + + // Associative array: key => label. + if ( empty( $value_key ) ) { + $options = array(); + foreach ( $items as $value => $label ) { + $options[] = array( + 'value' => $value, + 'label' => $label, + ); + } + return $options; + } + + // Array of arrays/objects: pluck by keys. + return array_map( + function ( $item ) use ( $value_key, $label_key ) { + $item = (array) $item; + return array( + 'value' => $item[ $value_key ] ?? '', + 'label' => $item[ $label_key ] ?? '', + ); + }, + array_values( $items ) + ); + } + + /** + * Prepare a single related model for response (belongs_to, has_one). + * + * @since 2.4.0 + * + * @param mixed $item Related model instance. + * @param string $controller Controller class name. + * @param WP_REST_Request $request Request object. + * + * @return array|null Prepared relation data or null. + */ + protected function prepare_relation( $item, string $controller, WP_REST_Request $request ): ?array { + if ( empty( $item ) ) { + return null; + } + + $ctrl = new $controller(); + + return $ctrl->prepare_response_for_collection( $ctrl->prepare_item_for_response( $item, $request ) ); + } + + /** + * Prepare a collection of related models for response (has_many, belongs_to_many). + * + * @since 2.4.0 + * + * @param array $items Array of related model instances. + * @param string $controller Controller class name. + * @param WP_REST_Request $request Request object. + * + * @return array Prepared relations data. + */ + protected function prepare_relations( array $items, string $controller, WP_REST_Request $request ): array { + if ( empty( $items ) ) { + return array(); + } + + $ctrl = new $controller(); + + return array_map( + fn( $item ) => $ctrl->prepare_response_for_collection( $ctrl->prepare_item_for_response( $item, $request ) ), + $items + ); + } + + /** + * Apply query filters from request. + * + * @since 2.4.0 + * + * @param mixed $query The query object. + * @param WP_REST_Request $request The REST API request. + * @param string $object_type Object type for filter hook (e.g., 'keys', 'activations'). + * + * @return mixed The modified query object. + */ + protected function apply_query_filters( $query, WP_REST_Request $request, string $object_type = '' ) { + $filters = $request->get_param( 'filters' ); + + if ( ! empty( $filters['filters'] ) && is_array( $filters['filters'] ) ) { + $boolean = 'any' === ( $filters['match'] ?? 'all' ) ? 'OR' : 'AND'; + $query->where( + function ( $q ) use ( $filters ) { + foreach ( $filters['filters'] as $filter ) { + if ( ! empty( $filter['field'] ) && ! empty( $filter['operator'] ) ) { + $q->where( $filter['field'], $filter['operator'], $filter['value'] ?? null ); + } + } + }, + $boolean + ); + } + + if ( $object_type ) { + /** + * Filters the query after applying filters. + * + * @since 2.4.0 + * + * @param mixed $query The query object. + * @param array $filters The filters array. + * @param WP_REST_Request $request The REST API request. + */ + $query = WCSN()->apply_filters( "rest_{$object_type}_apply_query_filters", $query, $filters, $request ); + } + + return $query; + } + + /** + * Process batch operations. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + protected function process_batch( $request ) { + $items = $request->get_params(); + $response = array(); + + /** + * Filters the maximum number of items allowed in a batch request. + * + * @since 2.4.0 + * + * @param int $limit Maximum batch size. Default 100. + */ + $limit = WCSN()->apply_filters( 'rest_batch_limit', 100 ); + $total = count( $items['create'] ?? array() ) + count( $items['update'] ?? array() ) + count( $items['delete'] ?? array() ); + + if ( $total > $limit ) { + // translators: %d is the maximum batch size. + return new WP_Error( 'rest_batch_too_large', sprintf( __( 'Batch limit is %d items.', 'wc-serial-numbers' ), $limit ), array( 'status' => 413 ) ); + } + + foreach ( $items['create'] ?? array() as $data ) { + $req = new WP_REST_Request( 'POST' ); + $req->set_body_params( $data ); + $res = $this->create_item( $req ); + $response['create'][] = is_wp_error( $res ) ? array( 'error' => $res->get_error_message() ) : $res->get_data(); + } + + foreach ( $items['update'] ?? array() as $data ) { + $req = new WP_REST_Request( 'PUT' ); + $req->set_body_params( $data ); + $res = $this->update_item( $req ); + $response['update'][] = is_wp_error( $res ) ? array( + 'id' => $data['id'] ?? 0, + 'error' => $res->get_error_message(), + ) : $res->get_data(); + } + + foreach ( $items['delete'] ?? array() as $id ) { + $id = is_array( $id ) ? ( $id['id'] ?? 0 ) : (int) $id; + $req = new WP_REST_Request( 'DELETE' ); + $req->set_param( 'id', $id ); + $res = $this->delete_item( $req ); + $response['delete'][] = is_wp_error( $res ) ? array( + 'id' => $id, + 'error' => $res->get_error_message(), + ) : $res->get_data(); + } + + return rest_ensure_response( $response ); + } +} diff --git a/includes/RestAPI/Controllers/Keys.php b/includes/RestAPI/Controllers/Keys.php new file mode 100644 index 00000000..f0fe37b4 --- /dev/null +++ b/includes/RestAPI/Controllers/Keys.php @@ -0,0 +1,382 @@ + rest_authorization_required_code() ) ); + } + + $query = Key::query(); + + foreach ( $request->get_params() as $key => $value ) { + $query->set_var( $key, $value ); + } + + $query = $this->apply_query_filters( $query, $request, 'keys' ); + + /** + * Filters the key query before execution. + * + * @since 2.4.0 + * + * @param mixed $query Query object. + * @param WP_REST_Request $request Request object. + */ + $query = WCSN()->apply_filters( 'rest_key_query', $query, $request ); + + $items = $query->get(); + $total = $query->count(); + $page = (int) $query->get_var( 'page', 1 ); + $per_page = (int) $query->get_var( 'per_page', 20 ); + + $results = array(); + foreach ( $items as $item ) { + $data = $this->prepare_item_for_response( $item, $request ); + $results[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $results ); + $max_pages = $total > 0 ? (int) ceil( $total / $per_page ) : 0; + + $response->header( 'X-WP-Total', (int) $total ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = add_query_arg( urlencode_deep( $request->get_query_params() ), rest_url( $request->get_route() ) ); + + if ( $page > 1 ) { + $prev_page = min( $page - 1, $max_pages ); + $response->link_header( 'prev', add_query_arg( 'page', $prev_page, $base ) ); + } + + if ( $max_pages > $page ) { + $response->link_header( 'next', add_query_arg( 'page', $page + 1, $base ) ); + } + + return $response; + } + + /** + * Retrieves one key. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + public function get_item( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce capability. + return new WP_Error( 'rest_forbidden', __( 'Not allowed.', 'wc-serial-numbers' ), array( 'status' => rest_authorization_required_code() ) ); + } + + $item = Key::find( $request->get_param( 'id' ) ); + + if ( ! $item ) { + return new WP_Error( 'rest_key_not_found', __( 'Key not found.', 'wc-serial-numbers' ), array( 'status' => 404 ) ); + } + + return $this->prepare_item_for_response( $item, $request ); + } + + /** + * Creates one key. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + public function create_item( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce capability. + return new WP_Error( 'rest_forbidden', __( 'Not allowed.', 'wc-serial-numbers' ), array( 'status' => rest_authorization_required_code() ) ); + } + + if ( ! empty( $request->get_param( 'id' ) ) ) { + return new WP_Error( 'rest_key_exists', __( 'Cannot create existing key.', 'wc-serial-numbers' ), array( 'status' => 400 ) ); + } + + $item = $this->prepare_item_for_database( $request ); + + if ( is_wp_error( $item ) ) { + return $item; + } + + /** + * Filters the key object before saving. + * + * @since 2.4.0 + * + * @param Key $item Key object. + * @param WP_REST_Request $request Request object. + */ + $item = WCSN()->apply_filters( 'rest_pre_insert_key', $item, $request ); + $item = $item->save(); + + if ( is_wp_error( $item ) ) { + return $item; + } + + $response = $this->prepare_item_for_response( $item, $request ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( $request->get_route() . '/' . $item->id ) ); + + return $response; + } + + /** + * Updates one key. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + public function update_item( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce capability. + return new WP_Error( 'rest_forbidden', __( 'Not allowed.', 'wc-serial-numbers' ), array( 'status' => rest_authorization_required_code() ) ); + } + + $item = $this->prepare_item_for_database( $request ); + + if ( is_wp_error( $item ) ) { + return $item; + } + + if ( ! $item->exists() ) { + return new WP_Error( 'rest_key_not_found', __( 'Key not found.', 'wc-serial-numbers' ), array( 'status' => 404 ) ); + } + + /** + * Filters the key object before saving. + * + * @since 2.4.0 + * + * @param Key $item Key object. + * @param WP_REST_Request $request Request object. + */ + $item = WCSN()->apply_filters( 'rest_pre_insert_key', $item, $request ); + $item = $item->save(); + + if ( is_wp_error( $item ) ) { + return $item; + } + + return $this->prepare_item_for_response( $item, $request ); + } + + /** + * Deletes one key. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + public function delete_item( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce capability. + return new WP_Error( 'rest_forbidden', __( 'Not allowed.', 'wc-serial-numbers' ), array( 'status' => rest_authorization_required_code() ) ); + } + + $item = Key::find( $request->get_param( 'id' ) ); + + if ( ! $item ) { + return new WP_Error( 'rest_key_not_found', __( 'Key not found.', 'wc-serial-numbers' ), array( 'status' => 404 ) ); + } + + $response = $this->prepare_item_for_response( $item, $request ); + $previous = $response->get_data(); + $deleted = $item->delete(); + + if ( is_wp_error( $deleted ) ) { + return $deleted; + } + + return rest_ensure_response( + array( + 'deleted' => true, + 'previous' => $previous, + ) + ); + } + + /** + * Retrieves form options for keys. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + public function get_options( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce capability. + return new WP_Error( 'rest_forbidden', __( 'Not allowed.', 'wc-serial-numbers' ), array( 'status' => rest_authorization_required_code() ) ); + } + + $options = array( + 'statuses' => $this->prepare_options( wcsn_get_key_statuses() ), + 'sources' => $this->prepare_options( wcsn_get_key_sources() ), + 'bulk_actions' => array( + array( + 'value' => 'delete', + 'label' => __( 'Delete', 'wc-serial-numbers' ), + ), + ), + 'import_fields' => array(), + 'export_columns' => array(), + ); + + /** + * Filters the key form options. + * + * @since 2.4.0 + * + * @param array $options Options data. + * @param WP_REST_Request $request Request object. + */ + return rest_ensure_response( WCSN()->apply_filters( 'rest_key_options', $options, $request ) ); + } + + /** + * Batch operations. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + public function batch_items( $request ) { + if ( ! current_user_can( 'manage_woocommerce' ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown -- WooCommerce capability. + return new WP_Error( 'rest_forbidden', __( 'Not allowed.', 'wc-serial-numbers' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return $this->process_batch( $request ); + } + + /** + * Import keys from CSV. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + public function import_items( $request ) { + return new WP_Error( 'rest_not_implemented', __( 'Import not yet implemented.', 'wc-serial-numbers' ), array( 'status' => 501 ) ); + } + + /** + * Export keys to CSV. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response|WP_Error Response or error. + */ + public function export_items( $request ) { + return new WP_Error( 'rest_not_implemented', __( 'Export not yet implemented.', 'wc-serial-numbers' ), array( 'status' => 501 ) ); + } + + /** + * Prepare key for response. + * + * @since 2.4.0 + * + * @param Key $item Key object. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response Response. + */ + public function prepare_item_for_response( $item, $request ): WP_REST_Response { + $raw = $item->to_array(); + $data = array(); + + foreach ( $raw as $key => $value ) { + switch ( $key ) { + case 'order_date': + case 'created_date': + $data[ $key ] = $this->prepare_date_response( $value ); + break; + + default: + $data[ $key ] = $value; + break; + } + } + + /** + * Filters the key data before creating the response. + * + * @since 2.4.0 + * + * @param WP_REST_Response $response Response object. + * @param Key $item Key object. + * @param WP_REST_Request $request Request object. + */ + return WCSN()->apply_filters( 'rest_prepare_key', rest_ensure_response( $data ), $item, $request ); + } + + /** + * Prepare key for database. + * + * @since 2.4.0 + * + * @param WP_REST_Request $request Request object. + * + * @return Key|WP_Error Key object or error. + */ + protected function prepare_item_for_database( $request ) { + $id = $request->get_param( 'id' ); + $item = $id ? Key::find( $id ) : new Key(); + + if ( $id && ! $item ) { + return new WP_Error( 'rest_key_not_found', __( 'Key not found.', 'wc-serial-numbers' ), array( 'status' => 404 ) ); + } + + foreach ( $request->get_params() as $key => $value ) { + switch ( $key ) { + case 'order_date': + case 'created_date': + $item->$key = $this->prepare_date_for_database( $value ); + break; + + default: + $item->$key = $value; + break; + } + } + + return $item; + } +} diff --git a/includes/RestAPI.php b/includes/RestAPI/Controllers/Software.php similarity index 96% rename from includes/RestAPI.php rename to includes/RestAPI/Controllers/Software.php index e00356ce..165cdde8 100644 --- a/includes/RestAPI.php +++ b/includes/RestAPI/Controllers/Software.php @@ -1,36 +1,27 @@ app->router; + + // Resources. + $router->resource( 'keys', Keys::class ); + $router->resource( 'activations', Activations::class ); + + // Software licensing endpoints, registered under the legacy unversioned namespace. + $this->app->make( Software::class )->register_routes(); + } +} diff --git a/includes/functions.php b/includes/functions.php index e13c80ec..a581db30 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -12,7 +12,7 @@ defined( 'ABSPATH' ) || exit; -require_once __DIR__ . '/Functions/Template.php'; +require_once __DIR__ . '/template.php'; /** * Get manager role. diff --git a/includes/Functions/Template.php b/includes/template.php similarity index 100% rename from includes/Functions/Template.php rename to includes/template.php diff --git a/wc-serial-numbers.php b/wc-serial-numbers.php index fe3199a6..7db45070 100644 --- a/wc-serial-numbers.php +++ b/wc-serial-numbers.php @@ -51,6 +51,8 @@ $data = array( 'version' => '2.4.0', 'short_name' => 'wc_serial_numbers', + 'rest_prefix' => 'wcsn', + 'rest_version' => 'v1', 'name' => 'Serial Numbers', 'plugin_uri' => 'https://pluginever.com/plugins/woocommerce-serial-numbers-pro/', 'settings_url' => admin_url( 'admin.php?page=wc-serial-numbers-settings' ), From 7c4fa67976e9211445e656e4d2192ca79daeadcd Mon Sep 17 00:00:00 2001 From: sultann Date: Thu, 11 Jun 2026 01:33:30 +0600 Subject: [PATCH 4/5] Rename the plugin namespace to PluginEver SerialNumbers with back-compat aliases and contract tests --- composer.json | 8 +- includes/API.php | 10 +- includes/Activations/Activations.php | 10 +- includes/Activations/Admin.php | 6 +- includes/Activations/ListTable.php | 8 +- includes/Admin/Admin.php | 6 +- includes/Admin/ListTables/ListTable.php | 4 +- includes/Admin/Menus.php | 19 +- includes/Admin/Reports.php | 6 +- includes/Admin/Settings.php | 6 +- includes/Admin/Tools.php | 6 +- includes/Cart.php | 8 +- includes/Compat.php | 6 +- includes/Encryption.php | 6 +- includes/Installer.php | 4 +- includes/Keys/Admin.php | 8 +- includes/Keys/Keys.php | 8 +- includes/Keys/ListTable.php | 8 +- includes/Models/Activation.php | 6 +- includes/Models/Key.php | 6 +- includes/Models/Model.php | 10 +- includes/Orders/Admin.php | 6 +- includes/Orders/Orders.php | 6 +- includes/Plugin.php | 4 +- includes/Products.php | 6 +- includes/RestAPI/Controllers/Activations.php | 6 +- includes/RestAPI/Controllers/Controller.php | 4 +- includes/RestAPI/Controllers/Keys.php | 6 +- includes/RestAPI/Controllers/Software.php | 8 +- includes/RestAPI/Routes.php | 12 +- includes/Shop.php | 6 +- includes/Stocks/ListTable.php | 6 +- includes/Stocks/Stocks.php | 8 +- includes/deprecated.php | 39 +- includes/functions.php | 8 +- includes/template.php | 4 +- languages/wc-serial-numbers.pot | 1658 +++++++++--------- templates/admin/api-actions.php | 2 +- templates/admin/api-validation.php | 2 +- templates/admin/edit-key.php | 4 +- templates/admin/list-activations.php | 4 +- templates/admin/list-keys.php | 4 +- templates/admin/list-stock.php | 4 +- templates/admin/notices/dependency.php | 2 +- templates/admin/reports.php | 2 +- templates/admin/tools.php | 2 +- templates/email-stock-notification.php | 2 +- tests/bootstrap.php | 2 +- tests/phpunit/ContractTest.php | 62 +- tests/phpunit/EncryptionTest.php | 4 +- tests/phpunit/KeyModelTest.php | 4 +- tests/phpunit/OrderDeliveryTest.php | 4 +- tests/phpunit/StocksTest.php | 2 +- tests/phpunit/TestCase.php | 4 +- wc-serial-numbers.php | 6 +- 55 files changed, 1105 insertions(+), 957 deletions(-) diff --git a/composer.json b/composer.json index e779e79f..05af48f8 100644 --- a/composer.json +++ b/composer.json @@ -19,12 +19,12 @@ }, "autoload": { "psr-4": { - "WooCommerceSerialNumbers\\": "includes/" + "PluginEver\\SerialNumbers\\": "includes/" } }, "autoload-dev": { "psr-4": { - "WooCommerceSerialNumbers\\Tests\\": "tests/phpunit/" + "PluginEver\\SerialNumbers\\Tests\\": "tests/phpunit/" } }, "config": { @@ -56,8 +56,8 @@ "extra": { "strauss": { "target_directory": "vendor", - "namespace_prefix": "WooCommerceSerialNumbers\\", - "classmap_prefix": "WooCommerceSerialNumbers__", + "namespace_prefix": "PluginEver\\SerialNumbers\\", + "classmap_prefix": "PluginEver_SerialNumbers__", "packages": [ "byteever/models", "byteever/plugin" diff --git a/includes/API.php b/includes/API.php index 099d8039..c8d180f8 100644 --- a/includes/API.php +++ b/includes/API.php @@ -1,10 +1,10 @@ PluginEver\SerialNumbers\Keys\ListTable::class, + 'WooCommerceSerialNumbers\\Admin\\ListTables\\ActivationsTable' => PluginEver\SerialNumbers\Activations\ListTable::class, + 'WooCommerceSerialNumbers\\Admin\\ListTables\\StockTable' => PluginEver\SerialNumbers\Stocks\ListTable::class, + 'WooCommerceSerialNumbers\\Admin\\Requests' => PluginEver\SerialNumbers\Keys\Admin::class, + 'WooCommerceSerialNumbers\\Admin\\Orders' => PluginEver\SerialNumbers\Orders\Admin::class, + 'WooCommerceSerialNumbers\\Admin\\Products' => PluginEver\SerialNumbers\Products::class, + 'WooCommerceSerialNumbers\\Frontend\\Frontend' => PluginEver\SerialNumbers\Shop::class, + 'WooCommerceSerialNumbers\\Frontend\\Shortcodes' => PluginEver\SerialNumbers\Shop::class, + ); + + $target = isset( $renamed[ $class_name ] ) + ? $renamed[ $class_name ] + : str_replace( 'WooCommerceSerialNumbers\\', 'PluginEver\\SerialNumbers\\', $class_name ); + + if ( class_exists( $target ) ) { + class_alias( $target, $class_name ); + } + }, + true, + true +); + /** * Get manager role. * @@ -424,7 +459,7 @@ function wc_serial_numbers_validate_boolean( $string ) { * * @since 1.5.6 * @deprecated 1.5.6 - * @return WooCommerceSerialNumbers\Plugin + * @return PluginEver\SerialNumbers\Plugin */ function wc_serial_numbers() { return WCSN(); diff --git a/includes/functions.php b/includes/functions.php index a581db30..7034c21e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -3,12 +3,12 @@ * Essential functions for the plugin. * * @since 1.0.0 - * @package WooCommerceSerialNumbers + * @package PluginEver\SerialNumbers */ -use WooCommerceSerialNumbers\Encryption; -use WooCommerceSerialNumbers\Models\Activation; -use WooCommerceSerialNumbers\Models\Key; +use PluginEver\SerialNumbers\Encryption; +use PluginEver\SerialNumbers\Models\Activation; +use PluginEver\SerialNumbers\Models\Key; defined( 'ABSPATH' ) || exit; diff --git a/includes/template.php b/includes/template.php index ab173581..31f2051c 100644 --- a/includes/template.php +++ b/includes/template.php @@ -3,10 +3,10 @@ * Template functions. * * @since 1.4.6 - * @package WooCommerceSerialNumbers/Functions + * @package PluginEver\SerialNumbers/Functions */ -use WooCommerceSerialNumbers\Models\Key; +use PluginEver\SerialNumbers\Models\Key; defined( 'ABSPATH' ) || exit; diff --git a/languages/wc-serial-numbers.pot b/languages/wc-serial-numbers.pot index 89a53c7b..9fbc95ae 100644 --- a/languages/wc-serial-numbers.pot +++ b/languages/wc-serial-numbers.pot @@ -9,20 +9,19 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-06-10T18:24:21+00:00\n" +"POT-Creation-Date: 2026-06-10T19:33:07+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: wc-serial-numbers\n" #. Plugin Name of the plugin #: wc-serial-numbers.php -#: includes/Admin/Admin.php:286 -#: includes/Admin/Menus.php:56 -#: includes/Admin/Menus.php:90 -#: includes/Admin/Menus.php:91 -#: includes/Admin/Menus.php:435 -#: includes/Admin/Products.php:94 -#: includes/Functions/Template.php:226 +#: includes/Admin/Admin.php:285 +#: includes/Admin/Menus.php:49 +#: includes/Admin/Menus.php:50 +#: includes/Admin/Tools.php:186 +#: includes/Products.php:104 +#: includes/template.php:226 msgid "Serial Numbers" msgstr "" @@ -46,1376 +45,1433 @@ msgstr "" msgid "https://pluginever.com/" msgstr "" +#: includes/Activations/ListTable.php:41 +#: includes/Keys/ListTable.php:358 +msgid "Activation" +msgstr "" + +#: includes/Activations/ListTable.php:42 +#: includes/Admin/Menus.php:80 +#: includes/Admin/Menus.php:81 +#: templates/admin/list-activations.php:18 +msgid "Activations" +msgstr "" + +#: includes/Activations/ListTable.php:102 +msgid "No activations found. Once a serial key is activated, it will appear here." +msgstr "" + +#: includes/Activations/ListTable.php:115 +#: includes/Keys/ListTable.php:283 +#: includes/Stocks/ListTable.php:84 +msgid "Filter" +msgstr "" + +#: includes/Activations/ListTable.php:166 +#: includes/Activations/ListTable.php:241 +#: includes/Keys/ListTable.php:338 +#: includes/Keys/ListTable.php:430 +#: includes/RestAPI/Controllers/Activations.php:248 +#: includes/RestAPI/Controllers/Keys.php:250 +#: templates/admin/edit-key.php:130 +msgid "Delete" +msgstr "" + +#: includes/Activations/ListTable.php:178 +#: includes/Shop.php:151 +#: templates/admin/api-actions.php:141 +msgid "Instance" +msgstr "" + +#: includes/Activations/ListTable.php:179 +#: includes/deprecated.php:394 +#: includes/Keys/ListTable.php:352 +#: includes/Shop.php:49 +#: includes/Shop.php:146 +#: includes/Stocks/ListTable.php:97 +#: templates/admin/api-actions.php:118 +#: templates/admin/api-validation.php:121 +#: templates/admin/edit-key.php:37 +msgid "Product" +msgstr "" + +#: includes/Activations/ListTable.php:180 +#: includes/Compat.php:91 +#: includes/functions.php:1015 +#: includes/Keys/ListTable.php:351 +#: includes/Orders/Admin.php:279 +#: includes/template.php:42 +#: templates/admin/api-actions.php:131 +#: templates/admin/api-validation.php:134 +msgid "Key" +msgstr "" + +#: includes/Activations/ListTable.php:181 +#: includes/Shop.php:153 +#: templates/admin/api-actions.php:152 +msgid "Platform" +msgstr "" + +#: includes/Activations/ListTable.php:182 +msgid "Activation Time" +msgstr "" + #. translators: %s: link to the plugin page -#: includes/Admin/Admin.php:78 +#: includes/Admin/Admin.php:77 #, php-format msgid "%s is not functional because you are using outdated version of the plugin, please update to the version 1.3.8 or higher." msgstr "" #. translators: 1: discount amount 2: promo code 3: link start 4: link end 5: plugin name 6: PRO label -#: includes/Admin/Admin.php:94 +#: includes/Admin/Admin.php:93 #, php-format msgid "Upgrade to %6$s to unlock the full potential of %5$s and avail a %1$s discount by using the promo code %2$s. %3$s Upgrade Now%4$s." msgstr "" -#: includes/Admin/Admin.php:119 -#: includes/Admin/Menus.php:172 -#: includes/Admin/Menus.php:173 +#: includes/Admin/Admin.php:118 +#: includes/Admin/Menus.php:131 +#: includes/Admin/Menus.php:132 msgid "Settings" msgstr "" -#: includes/Admin/Admin.php:137 +#: includes/Admin/Admin.php:136 msgid "Go Pro" msgstr "" -#: includes/Admin/Admin.php:160 +#: includes/Admin/Admin.php:159 #: includes/Admin/Settings.php:69 msgid "Documentation" msgstr "" -#: includes/Admin/Admin.php:166 +#: includes/Admin/Admin.php:165 msgid "Support" msgstr "" -#: includes/Admin/Admin.php:172 +#: includes/Admin/Admin.php:171 msgid "Review" msgstr "" -#: includes/Admin/Admin.php:178 +#: includes/Admin/Admin.php:177 msgid "More Plugins" msgstr "" -#: includes/Admin/Admin.php:211 +#: includes/Admin/Admin.php:210 msgid "Search by product" msgstr "" -#: includes/Admin/Admin.php:212 +#: includes/Admin/Admin.php:211 msgid "Search by order" msgstr "" -#: includes/Admin/Admin.php:213 +#: includes/Admin/Admin.php:212 msgid "Search by customer" msgstr "" -#: includes/Admin/Admin.php:214 +#: includes/Admin/Admin.php:213 msgid "Show" msgstr "" -#: includes/Admin/Admin.php:215 +#: includes/Admin/Admin.php:214 msgid "Hide" msgstr "" -#: includes/Admin/Admin.php:216 -#: includes/Frontend/Frontend.php:54 +#: includes/Admin/Admin.php:215 +#: includes/Shop.php:348 msgid "Copied" msgstr "" #. translators: 1: Plugin name 2: WordPress -#: includes/Admin/Admin.php:253 +#: includes/Admin/Admin.php:252 #, php-format msgid "Thank you for using %1$s! Share your appreciation with a five-star review %2$s." msgstr "" -#: includes/Admin/Admin.php:255 +#: includes/Admin/Admin.php:254 msgid "Thanks :)" msgstr "" #. translators: 1: Plugin version -#: includes/Admin/Admin.php:273 +#: includes/Admin/Admin.php:272 #, php-format msgid "Version %s" msgstr "" -#: includes/Admin/ListTables/ActivationsTable.php:39 -#: includes/Admin/ListTables/KeysTable.php:356 -msgid "Activation" +#: includes/Admin/ListTables/ListTable.php:140 +#: includes/Admin/ListTables/ListTable.php:142 +msgid "Filter by order" +msgstr "" + +#: includes/Admin/ListTables/ListTable.php:163 +msgid "Filter by product" +msgstr "" + +#: includes/Admin/ListTables/ListTable.php:186 +msgid "Filter by customer" +msgstr "" + +#: includes/Admin/Menus.php:60 +#: includes/Admin/Menus.php:61 +#: templates/admin/list-keys.php:18 +msgid "Serial Keys" +msgstr "" + +#: includes/Admin/Menus.php:97 +#: includes/Admin/Menus.php:98 +msgid "Tools" +msgstr "" + +#: includes/Admin/Menus.php:114 +#: includes/Admin/Menus.php:115 +msgid "Reports" +msgstr "" + +#: includes/Admin/Menus.php:150 +#: includes/Products.php:205 +#: includes/Products.php:226 +msgid "Upgrade to Pro" +msgstr "" + +#: includes/Admin/Reports.php:36 +#: includes/Stocks/ListTable.php:100 +msgid "Stock" +msgstr "" + +#: includes/Admin/Settings.php:86 +msgid "General" +msgstr "" + +#: includes/Admin/Settings.php:120 +msgid "General Settings" +msgstr "" + +#: includes/Admin/Settings.php:122 +msgid "These options determine the behavior and operation of the plugin." +msgstr "" + +#: includes/Admin/Settings.php:126 +msgid "Auto-complete orders" +msgstr "" + +#: includes/Admin/Settings.php:128 +msgid "Automatically completes orders after successful payments." +msgstr "" + +#: includes/Admin/Settings.php:133 +msgid "Reuse keys" +msgstr "" + +#: includes/Admin/Settings.php:135 +msgid "Recover failed, refunded keys for selling again." +msgstr "" + +#: includes/Admin/Settings.php:136 +msgid "If you enable this option, the keys will be available for selling again if the order is refunded or failed." +msgstr "" + +#: includes/Admin/Settings.php:142 +msgid "Revoke keys" +msgstr "" + +#: includes/Admin/Settings.php:144 +msgid "Revoke keys when the order status changes to cancelled or refunded." +msgstr "" + +#: includes/Admin/Settings.php:145 +msgid "If you enable this option, the keys will be revoked when the order status changes to cancelled or refunded." +msgstr "" + +#: includes/Admin/Settings.php:150 +msgid "Hide keys" +msgstr "" + +#: includes/Admin/Settings.php:152 +msgid "Keys will be masked in the list table." +msgstr "" + +#: includes/Admin/Settings.php:157 +msgid "Disable software support" +msgstr "" + +#: includes/Admin/Settings.php:159 +msgid "Disable Software Licensing support & API functionalities." +msgstr "" + +#: includes/Admin/Settings.php:160 +msgid "If you enable this option, the activation menu and it’s functionality will be turned off." +msgstr "" + +#: includes/Admin/Settings.php:166 +msgid "Manage Stocks" +msgstr "" + +#: includes/Admin/Settings.php:168 +msgid "Manage stocks for the key enabled products." +msgstr "" + +#: includes/Admin/Settings.php:169 +msgid "Enable stock management for key-enabled products. This works only if you select \"Manually Added\" as the key source and enable stock management for the product. Variable product is not supported." +msgstr "" + +#: includes/Admin/Settings.php:175 +msgid "WooCommerce PDF Invoices" +msgstr "" + +#: includes/Admin/Settings.php:177 +msgid "Enable WooCommerce PDF Invoices." +msgstr "" + +#. translators: %s: documentation link +#: includes/Admin/Settings.php:180 +#, php-format +msgid "If you enable this option, the plugin will be compatible with WooCommerce PDF Invoices & Packing Slips plugins and will show the serial keys in the invoice. Check out the documentation for more details." +msgstr "" + +#: includes/Admin/Settings.php:191 +msgid "Stock Notification" +msgstr "" + +#: includes/Admin/Settings.php:193 +msgid "These options determine the operation of the key's stock notification." +msgstr "" + +#: includes/Admin/Settings.php:197 +msgid "Stock notification email" +msgstr "" + +#: includes/Admin/Settings.php:199 +msgid "Sends notification emails when key stock is low." +msgstr "" + +#: includes/Admin/Settings.php:205 +msgid "Stock threshold" +msgstr "" + +#: includes/Admin/Settings.php:207 +msgid "An email notification will be sent when the key stock falls below the specified number." +msgstr "" + +#: includes/Admin/Settings.php:212 +msgid "Notification recipient email" +msgstr "" + +#: includes/Admin/Settings.php:214 +msgid "The email address which will be used to send email notifications." +msgstr "" + +#: includes/Admin/Settings.php:287 +msgid "Create and assign keys for WooCommerce variable products." +msgstr "" + +#: includes/Admin/Settings.php:288 +msgid "Generate bulk keys with your custom key generator rule." +msgstr "" + +#: includes/Admin/Settings.php:289 +msgid "Random & sequential key order for the generator rules." +msgstr "" + +#: includes/Admin/Settings.php:290 +msgid "Automatic key generator to auto-create & assign keys with orders." +msgstr "" + +#: includes/Admin/Settings.php:291 +msgid "License key management option from the order page with required actions." +msgstr "" + +#: includes/Admin/Settings.php:292 +msgid "Support for bulk import/export of keys from/to CSV." +msgstr "" + +#: includes/Admin/Settings.php:293 +msgid "Send keys via SMS with Twilio." +msgstr "" + +#: includes/Admin/Settings.php:294 +msgid "Option to sell keys even if there are no available keys in the stock." +msgstr "" + +#: includes/Admin/Settings.php:295 +msgid "Custom deliverable quantity to deliver multiple keys with a single product." +msgstr "" + +#: includes/Admin/Settings.php:296 +msgid "Manual delivery option to manually deliver license keys instead of automatic." +msgstr "" + +#: includes/Admin/Settings.php:297 +msgid "Email template to easily and quickly customize the order confirmation & low stock alert email." +msgstr "" + +#: includes/Admin/Settings.php:298 +msgid "Many more ..." +msgstr "" + +#: includes/Admin/Settings.php:302 +msgid "Want More?" +msgstr "" + +#: includes/Admin/Settings.php:303 +msgid "This plugin offers a premium version which comes with the following features:" +msgstr "" + +#: includes/Admin/Settings.php:309 +msgid "Upgrade to PRO" +msgstr "" + +#: includes/Admin/Tools.php:39 +#: includes/Admin/Tools.php:134 +msgid "Generators" +msgstr "" + +#: includes/Admin/Tools.php:40 +msgid "API Toolkit" +msgstr "" + +#: includes/Admin/Tools.php:41 +#: templates/admin/list-keys.php:25 +msgid "Import" +msgstr "" + +#: includes/Admin/Tools.php:42 +#: templates/admin/list-keys.php:29 +msgid "Export" +msgstr "" + +#: includes/Admin/Tools.php:73 +#: includes/Compat.php:111 +#: includes/functions.php:1040 +#: includes/Keys/ListTable.php:362 +#: includes/Orders/Admin.php:291 +#: includes/template.php:78 +#: templates/admin/edit-key.php:92 +msgid "Status" +msgstr "" + +#: includes/Admin/Tools.php:88 +#: includes/Admin/Tools.php:95 +#: includes/Admin/Tools.php:113 +#: includes/Admin/Tools.php:131 +msgid "Available in Pro Version" +msgstr "" + +#: includes/Admin/Tools.php:89 +#: includes/Admin/Tools.php:96 +#: includes/Admin/Tools.php:114 +#: includes/Admin/Tools.php:132 +msgid "Upgrade to Pro Now" +msgstr "" + +#: includes/Admin/Tools.php:91 +#: includes/Admin/Tools.php:98 +msgid "Import Serial Numbers" +msgstr "" + +#: includes/Admin/Tools.php:116 +msgid "Export Serial Numbers" +msgstr "" + +#: includes/Admin/Tools.php:161 +msgid "Table exists" +msgstr "" + +#: includes/Admin/Tools.php:163 +msgid "Table does not exist" +msgstr "" + +#: includes/Admin/Tools.php:169 +msgid "Hourly cron" +msgstr "" + +#: includes/Admin/Tools.php:170 +msgid "Daily cron" +msgstr "" + +#. translators: %s: Next scheduled time. +#: includes/Admin/Tools.php:176 +#, php-format +msgid "Next run: %s" +msgstr "" + +#: includes/Admin/Tools.php:178 +msgid "Not scheduled" +msgstr "" + +#: includes/API.php:80 +msgid "Missing data." +msgstr "" + +#. translators: %1$s: product title, %2$s: stock quantity. +#: includes/Cart.php:58 +#, php-format +msgid "Sorry, there aren’t enough Serial Keys for %1$s. Please remove this item or lower the quantity. For now, we have %2$s Serial Keys for this product." +msgstr "" + +#: includes/Compat.php:86 +#: includes/Orders/Admin.php:274 +msgid "Serial keys sold with this product:" +msgstr "" + +#: includes/Compat.php:95 +#: includes/Orders/Admin.php:283 +msgid "Expire date" +msgstr "" + +#: includes/Compat.php:96 +#: includes/functions.php:1036 +#: includes/Keys/ListTable.php:534 +#: includes/Orders/Admin.php:284 +#: includes/template.php:63 +msgid "Lifetime" +msgstr "" + +#: includes/Compat.php:99 +#: includes/Orders/Admin.php:287 +#: templates/admin/edit-key.php:67 +msgid "Activation limit" +msgstr "" + +#: includes/Compat.php:100 +#: includes/Orders/Admin.php:288 +msgid "Unlimited" +msgstr "" + +#: includes/Compat.php:103 +msgid "Activation count" +msgstr "" + +#: includes/Compat.php:107 +msgid "Activation email" +msgstr "" + +#: includes/deprecated.php:189 +msgid "Serial number not found." +msgstr "" + +#: includes/deprecated.php:395 +msgid "Serial Number" +msgstr "" + +#: includes/deprecated.php:396 +#: includes/Shop.php:52 +#: includes/Shop.php:149 +#: templates/admin/api-actions.php:162 +#: templates/admin/api-validation.php:144 +#: templates/admin/edit-key.php:154 +msgid "Email" +msgstr "" + +#: includes/deprecated.php:397 +#: includes/functions.php:1025 +#: includes/template.php:52 +msgid "Activation Limit" +msgstr "" + +#: includes/deprecated.php:398 +msgid "Expires" +msgstr "" + +#: includes/functions.php:46 +#: includes/Keys/ListTable.php:180 +#: includes/Keys/ListTable.php:237 +msgid "Available" +msgstr "" + +#: includes/functions.php:47 +#: includes/Keys/ListTable.php:185 +#: includes/Keys/ListTable.php:244 +msgid "Pending" +msgstr "" + +#: includes/functions.php:48 +#: includes/Keys/ListTable.php:190 +#: includes/Keys/ListTable.php:251 +#: includes/Stocks/ListTable.php:99 +msgid "Sold" +msgstr "" + +#: includes/functions.php:49 +#: includes/Keys/ListTable.php:195 +#: includes/Keys/ListTable.php:258 +#: includes/template.php:72 +msgid "Expired" +msgstr "" + +#: includes/functions.php:50 +#: includes/Keys/ListTable.php:200 +#: includes/Keys/ListTable.php:265 +msgid "Cancelled" +msgstr "" + +#: includes/functions.php:91 +msgid "Manually added" +msgstr "" + +#. translators: 1: product title 2: source and 3: Quantity +#: includes/functions.php:539 +#, php-format +msgid "There is not enough serial numbers for the product %1$s from selected source %2$s, needed total %3$d." +msgstr "" + +#: includes/functions.php:1020 +#: includes/template.php:47 +msgid "Activation Email" msgstr "" -#: includes/Admin/ListTables/ActivationsTable.php:40 -#: includes/Admin/Menus.php:121 -#: includes/Admin/Menus.php:122 -#: includes/Admin/views/html-list-activations.php:18 -msgid "Activations" +#: includes/functions.php:1026 +#: includes/functions.php:1031 +#: includes/template.php:53 +#: includes/template.php:58 +msgid "None" msgstr "" -#: includes/Admin/ListTables/ActivationsTable.php:100 -msgid "No activations found. Once a serial key is activated, it will appear here." +#: includes/functions.php:1030 +#: includes/template.php:57 +msgid "Activation Count" msgstr "" -#: includes/Admin/ListTables/ActivationsTable.php:113 -#: includes/Admin/ListTables/KeysTable.php:281 -#: includes/Admin/ListTables/StockTable.php:82 -msgid "Filter" +#: includes/functions.php:1035 +#: includes/template.php:62 +msgid "Expire Date" msgstr "" -#: includes/Admin/ListTables/ActivationsTable.php:164 -#: includes/Admin/ListTables/ActivationsTable.php:239 -#: includes/Admin/ListTables/KeysTable.php:336 -#: includes/Admin/ListTables/KeysTable.php:428 -#: includes/Admin/views/html-edit-key.php:130 -msgid "Delete" +#: includes/Installer.php:60 +msgid "Once a Minute" msgstr "" -#: includes/Admin/ListTables/ActivationsTable.php:176 -#: includes/Admin/views/html-api-actions.php:141 -#: includes/Frontend/Shortcodes.php:148 -msgid "Instance" +#: includes/Keys/Admin.php:81 +#: includes/Orders/Admin.php:81 +#: includes/Orders/Admin.php:220 +msgid "You do not have permission to perform this action." msgstr "" -#: includes/Admin/ListTables/ActivationsTable.php:177 -#: includes/Admin/ListTables/KeysTable.php:350 -#: includes/Admin/ListTables/StockTable.php:95 -#: includes/Admin/views/html-api-actions.php:118 -#: includes/Admin/views/html-api-validation.php:121 -#: includes/Admin/views/html-edit-key.php:37 -#: includes/deprecated.php:359 -#: includes/Frontend/Shortcodes.php:46 -#: includes/Frontend/Shortcodes.php:143 -msgid "Product" +#: includes/Keys/Admin.php:124 +msgid "Key added successfully." msgstr "" -#: includes/Admin/ListTables/ActivationsTable.php:178 -#: includes/Admin/ListTables/KeysTable.php:349 -#: includes/Admin/Orders.php:269 -#: includes/Admin/views/html-api-actions.php:131 -#: includes/Admin/views/html-api-validation.php:134 -#: includes/Compat.php:91 -#: includes/functions.php:1015 -#: includes/Functions/Template.php:42 -msgid "Key" +#: includes/Keys/Admin.php:126 +msgid "Key updated successfully." msgstr "" -#: includes/Admin/ListTables/ActivationsTable.php:179 -#: includes/Admin/views/html-api-actions.php:152 -#: includes/Frontend/Shortcodes.php:150 -msgid "Platform" +#: includes/Keys/Admin.php:145 +#: includes/Keys/Admin.php:208 +#: includes/Keys/Admin.php:283 +msgid "You do not have permission to access this endpoint." msgstr "" -#: includes/Admin/ListTables/ActivationsTable.php:180 -msgid "Activation Time" +#. translators: $1: customer name, $2 customer id, $3: customer email +#: includes/Keys/Admin.php:319 +#, php-format +msgid "%1$s (#%2$s - %3$s)" msgstr "" -#: includes/Admin/ListTables/KeysTable.php:78 +#: includes/Keys/ListTable.php:80 msgid "key" msgstr "" -#: includes/Admin/ListTables/KeysTable.php:79 +#: includes/Keys/ListTable.php:81 msgid "keys" msgstr "" -#: includes/Admin/ListTables/KeysTable.php:170 +#: includes/Keys/ListTable.php:172 msgid "No keys found." msgstr "" -#: includes/Admin/ListTables/KeysTable.php:170 +#: includes/Keys/ListTable.php:172 msgid "Add new key" msgstr "" -#: includes/Admin/ListTables/KeysTable.php:174 +#: includes/Keys/ListTable.php:176 msgid "Keys can have one of the following statuses:" msgstr "" -#: includes/Admin/ListTables/KeysTable.php:178 -#: includes/Admin/ListTables/KeysTable.php:235 -#: includes/functions.php:46 -msgid "Available" -msgstr "" - -#: includes/Admin/ListTables/KeysTable.php:180 +#: includes/Keys/ListTable.php:182 msgid "This means the key is available for purchase." msgstr "" -#: includes/Admin/ListTables/KeysTable.php:183 -#: includes/Admin/ListTables/KeysTable.php:242 -#: includes/functions.php:47 -msgid "Pending" -msgstr "" - -#: includes/Admin/ListTables/KeysTable.php:185 +#: includes/Keys/ListTable.php:187 msgid "This means the key has been sold, but the order has not been completed yet." msgstr "" -#: includes/Admin/ListTables/KeysTable.php:188 -#: includes/Admin/ListTables/KeysTable.php:249 -#: includes/Admin/ListTables/StockTable.php:97 -#: includes/functions.php:48 -msgid "Sold" -msgstr "" - -#: includes/Admin/ListTables/KeysTable.php:190 +#: includes/Keys/ListTable.php:192 msgid "This means the key has been sold, and the order has been completed." msgstr "" -#: includes/Admin/ListTables/KeysTable.php:193 -#: includes/Admin/ListTables/KeysTable.php:256 -#: includes/functions.php:49 -#: includes/Functions/Template.php:72 -msgid "Expired" -msgstr "" - -#: includes/Admin/ListTables/KeysTable.php:195 +#: includes/Keys/ListTable.php:197 msgid "This means the key has expired and is no longer valid." msgstr "" -#: includes/Admin/ListTables/KeysTable.php:198 -#: includes/Admin/ListTables/KeysTable.php:263 -#: includes/functions.php:50 -msgid "Cancelled" -msgstr "" - -#: includes/Admin/ListTables/KeysTable.php:200 +#: includes/Keys/ListTable.php:202 msgid "This means the key has been cancelled and is no longer available for purchase or use." msgstr "" -#: includes/Admin/ListTables/KeysTable.php:226 +#: includes/Keys/ListTable.php:228 msgid "All keys." msgstr "" -#: includes/Admin/ListTables/KeysTable.php:228 +#: includes/Keys/ListTable.php:230 msgid "All" msgstr "" -#: includes/Admin/ListTables/KeysTable.php:233 +#: includes/Keys/ListTable.php:235 msgid "Available for sell." msgstr "" -#: includes/Admin/ListTables/KeysTable.php:240 +#: includes/Keys/ListTable.php:242 msgid "Pending payment." msgstr "" -#: includes/Admin/ListTables/KeysTable.php:247 +#: includes/Keys/ListTable.php:249 msgid "Sold keys." msgstr "" -#: includes/Admin/ListTables/KeysTable.php:254 +#: includes/Keys/ListTable.php:256 msgid "Expired keys." msgstr "" -#: includes/Admin/ListTables/KeysTable.php:261 +#: includes/Keys/ListTable.php:263 msgid "Cancelled keys." msgstr "" -#: includes/Admin/ListTables/KeysTable.php:337 +#: includes/Keys/ListTable.php:339 msgid "Reset Activations" msgstr "" -#: includes/Admin/ListTables/KeysTable.php:351 +#: includes/Keys/ListTable.php:353 msgid "Order" msgstr "" -#: includes/Admin/ListTables/KeysTable.php:352 +#: includes/Keys/ListTable.php:354 msgid "Validity" msgstr "" -#: includes/Admin/ListTables/KeysTable.php:359 +#: includes/Keys/ListTable.php:361 msgid "Order Date" msgstr "" -#: includes/Admin/ListTables/KeysTable.php:360 -#: includes/Admin/Menus.php:322 -#: includes/Admin/Orders.php:281 -#: includes/Admin/views/html-edit-key.php:92 -#: includes/Compat.php:111 -#: includes/functions.php:1040 -#: includes/Functions/Template.php:78 -msgid "Status" -msgstr "" - #. translators: %d: key id. -#: includes/Admin/ListTables/KeysTable.php:426 +#: includes/Keys/ListTable.php:428 #, php-format msgid "ID: %d" msgstr "" -#: includes/Admin/ListTables/KeysTable.php:427 -#: includes/Admin/ListTables/StockTable.php:143 +#: includes/Keys/ListTable.php:429 +#: includes/Stocks/ListTable.php:145 msgid "Edit" msgstr "" #. translators: %1$s: validity, %2$s: validity. -#: includes/Admin/ListTables/KeysTable.php:522 +#: includes/Keys/ListTable.php:524 #, php-format msgid "%s Day
After purchase" msgid_plural "%s Days
After purchase" msgstr[0] "" msgstr[1] "" -#: includes/Admin/ListTables/KeysTable.php:532 -#: includes/Admin/Orders.php:274 -#: includes/Compat.php:96 -#: includes/functions.php:1036 -#: includes/Functions/Template.php:63 -msgid "Lifetime" -msgstr "" - -#: includes/Admin/ListTables/ListTable.php:140 -#: includes/Admin/ListTables/ListTable.php:142 -msgid "Filter by order" -msgstr "" - -#: includes/Admin/ListTables/ListTable.php:163 -msgid "Filter by product" -msgstr "" - -#: includes/Admin/ListTables/ListTable.php:186 -msgid "Filter by customer" -msgstr "" - -#: includes/Admin/ListTables/StockTable.php:20 -msgid "stock" -msgstr "" - -#: includes/Admin/ListTables/StockTable.php:21 -msgid "stocks" -msgstr "" - -#: includes/Admin/ListTables/StockTable.php:70 -msgid "No products selling serial keys from \"stock\" found." -msgstr "" - -#: includes/Admin/ListTables/StockTable.php:96 -msgid "Source" -msgstr "" - -#: includes/Admin/ListTables/StockTable.php:98 -#: includes/Admin/Menus.php:282 -msgid "Stock" -msgstr "" - -#: includes/Admin/ListTables/StockTable.php:172 -msgid "Manual" -msgstr "" - -#: includes/Admin/ListTables/StockTable.php:174 -msgid "Generator Rule" -msgstr "" - -#: includes/Admin/ListTables/StockTable.php:176 -msgid "Auto Generated" -msgstr "" - -#: includes/Admin/ListTables/StockTable.php:178 -msgid "Unknown" -msgstr "" - -#: includes/Admin/Menus.php:101 -#: includes/Admin/Menus.php:102 -#: includes/Admin/views/html-list-keys.php:18 -msgid "Serial Keys" -msgstr "" - -#: includes/Admin/Menus.php:138 -#: includes/Admin/Menus.php:139 -msgid "Tools" -msgstr "" - -#: includes/Admin/Menus.php:155 -#: includes/Admin/Menus.php:156 -msgid "Reports" -msgstr "" - -#: includes/Admin/Menus.php:191 -#: includes/Admin/Products.php:195 -#: includes/Admin/Products.php:216 -msgid "Upgrade to Pro" -msgstr "" - -#: includes/Admin/Menus.php:247 -#: includes/Admin/Menus.php:383 -msgid "Generators" -msgstr "" - -#: includes/Admin/Menus.php:248 -msgid "API Toolkit" -msgstr "" - -#: includes/Admin/Menus.php:249 -#: includes/Admin/views/html-list-keys.php:25 -msgid "Import" -msgstr "" - -#: includes/Admin/Menus.php:250 -#: includes/Admin/views/html-list-keys.php:29 -msgid "Export" -msgstr "" - -#: includes/Admin/Menus.php:337 -#: includes/Admin/Menus.php:344 -#: includes/Admin/Menus.php:362 -#: includes/Admin/Menus.php:380 -msgid "Available in Pro Version" -msgstr "" - -#: includes/Admin/Menus.php:338 -#: includes/Admin/Menus.php:345 -#: includes/Admin/Menus.php:363 -#: includes/Admin/Menus.php:381 -msgid "Upgrade to Pro Now" -msgstr "" - -#: includes/Admin/Menus.php:340 -#: includes/Admin/Menus.php:347 -msgid "Import Serial Numbers" -msgstr "" - -#: includes/Admin/Menus.php:365 -msgid "Export Serial Numbers" +#: includes/Models/Activation.php:292 +msgid "Serial id is required." msgstr "" -#: includes/Admin/Menus.php:410 -msgid "Table exists" +#: includes/Models/Activation.php:297 +msgid "Instance is required." msgstr "" -#: includes/Admin/Menus.php:412 -msgid "Table does not exist" +#: includes/Models/Key.php:578 +msgid "Product id is required." msgstr "" -#: includes/Admin/Menus.php:418 -msgid "Hourly cron" +#: includes/Models/Key.php:583 +msgid "Product id is invalid." msgstr "" -#: includes/Admin/Menus.php:419 -msgid "Daily cron" +#: includes/Models/Key.php:588 +#: includes/RestAPI/Controllers/Software.php:143 +msgid "Serial key is required." msgstr "" -#. translators: %s: Next scheduled time. -#: includes/Admin/Menus.php:425 -#, php-format -msgid "Next run: %s" +#: includes/Models/Key.php:600 +msgid "Serial key already exists. Duplicate serial keys are not allowed." msgstr "" -#: includes/Admin/Menus.php:427 -msgid "Not scheduled" +#: includes/Models/Key.php:606 +msgid "Order id is invalid." msgstr "" -#: includes/Admin/Orders.php:55 -#: includes/Admin/Orders.php:191 +#: includes/Orders/Admin.php:65 +#: includes/Orders/Admin.php:201 msgid "Add serial keys" msgstr "" -#: includes/Admin/Orders.php:56 -#: includes/Admin/Orders.php:192 +#: includes/Orders/Admin.php:66 +#: includes/Orders/Admin.php:202 msgid "Remove serial keys" msgstr "" -#: includes/Admin/Orders.php:71 -#: includes/Admin/Orders.php:210 -#: includes/Admin/Requests.php:44 -msgid "You do not have permission to perform this action." -msgstr "" - -#: includes/Admin/Orders.php:81 +#: includes/Orders/Admin.php:91 msgid "Serial keys added successfully to the order." msgstr "" -#: includes/Admin/Orders.php:85 +#: includes/Orders/Admin.php:95 msgid "Serial keys removed successfully from the order." msgstr "" -#: includes/Admin/Orders.php:122 -#: includes/Admin/Orders.php:162 +#: includes/Orders/Admin.php:132 +#: includes/Orders/Admin.php:172 msgid "Order is fullfilled and completed." msgstr "" -#: includes/Admin/Orders.php:125 -#: includes/Admin/Orders.php:165 +#: includes/Orders/Admin.php:135 +#: includes/Orders/Admin.php:175 msgid "Order is fullfilled and processing." msgstr "" -#: includes/Admin/Orders.php:128 -#: includes/Admin/Orders.php:168 +#: includes/Orders/Admin.php:138 +#: includes/Orders/Admin.php:178 msgid "Order is fullfilled and on-hold." msgstr "" -#: includes/Admin/Orders.php:131 -#: includes/Admin/Orders.php:171 +#: includes/Orders/Admin.php:141 +#: includes/Orders/Admin.php:181 msgid "Order is fullfilled and draft." msgstr "" -#: includes/Admin/Orders.php:134 -#: includes/Admin/Orders.php:174 +#: includes/Orders/Admin.php:144 +#: includes/Orders/Admin.php:184 msgid "Order is not fullfilled." msgstr "" #. Translators: %d: number of orders. -#: includes/Admin/Orders.php:226 +#: includes/Orders/Admin.php:236 #, php-format msgid "%d orders updated successfully." msgstr "" -#: includes/Admin/Orders.php:264 -#: includes/Compat.php:86 -msgid "Serial keys sold with this product:" -msgstr "" - -#: includes/Admin/Orders.php:273 -#: includes/Compat.php:95 -msgid "Expire date" -msgstr "" - -#: includes/Admin/Orders.php:277 -#: includes/Admin/views/html-edit-key.php:67 -#: includes/Compat.php:99 -msgid "Activation limit" -msgstr "" - -#: includes/Admin/Orders.php:278 -#: includes/Compat.php:100 -msgid "Unlimited" +#: includes/Orders/Admin.php:318 +msgid "View Details" msgstr "" -#: includes/Admin/Orders.php:308 -msgid "View Details" +#: includes/Orders/Orders.php:92 +msgid "Order automatically completed by the Serial Numbers for WooCommerce." msgstr "" -#: includes/Admin/Products.php:116 +#: includes/Products.php:126 msgid "Sell keys" msgstr "" -#: includes/Admin/Products.php:117 +#: includes/Products.php:127 msgid "Enable this if you are selling keys with this product." msgstr "" -#: includes/Admin/Products.php:130 +#: includes/Products.php:140 msgid "Delivery quantity" msgstr "" -#: includes/Admin/Products.php:131 +#: includes/Products.php:141 msgid "Number of key(s) will be delivered per item. Available in PRO." msgstr "" -#: includes/Admin/Products.php:151 -#: includes/Admin/Products.php:183 +#: includes/Products.php:161 +#: includes/Products.php:193 msgid "Key source" msgstr "" -#: includes/Admin/Products.php:169 +#: includes/Products.php:179 msgid "Software version" msgstr "" -#: includes/Admin/Products.php:170 +#: includes/Products.php:180 msgid "Version number for the software. Ignore if it's not a software." msgstr "" -#: includes/Admin/Products.php:171 +#: includes/Products.php:181 msgid "e.g. 1.0" msgstr "" -#: includes/Admin/Products.php:185 +#: includes/Products.php:195 msgid "key available." msgid_plural "keys available." msgstr[0] "" msgstr[1] "" -#: includes/Admin/Products.php:193 +#: includes/Products.php:203 msgid "Want to sell keys for variable products?" msgstr "" -#: includes/Admin/Products.php:214 +#: includes/Products.php:224 msgid "The free version of Serial Numbers for WooCommerce does not support product variation." msgstr "" -#: includes/Admin/Products.php:233 +#: includes/Products.php:243 msgid "You do not have permission to save this data." msgstr "" -#: includes/Admin/Requests.php:87 -msgid "Key added successfully." +#: includes/RestAPI/Controllers/Activations.php:31 +#: includes/RestAPI/Controllers/Activations.php:94 +#: includes/RestAPI/Controllers/Activations.php:117 +#: includes/RestAPI/Controllers/Activations.php:163 +#: includes/RestAPI/Controllers/Activations.php:205 +#: includes/RestAPI/Controllers/Activations.php:241 +#: includes/RestAPI/Controllers/Activations.php:277 +#: includes/RestAPI/Controllers/Keys.php:31 +#: includes/RestAPI/Controllers/Keys.php:94 +#: includes/RestAPI/Controllers/Keys.php:117 +#: includes/RestAPI/Controllers/Keys.php:163 +#: includes/RestAPI/Controllers/Keys.php:205 +#: includes/RestAPI/Controllers/Keys.php:241 +#: includes/RestAPI/Controllers/Keys.php:279 +msgid "Not allowed." msgstr "" -#: includes/Admin/Requests.php:89 -msgid "Key updated successfully." +#: includes/RestAPI/Controllers/Activations.php:100 +#: includes/RestAPI/Controllers/Activations.php:173 +#: includes/RestAPI/Controllers/Activations.php:211 +#: includes/RestAPI/Controllers/Activations.php:361 +msgid "Activation not found." msgstr "" -#: includes/Admin/Requests.php:108 -#: includes/Admin/Requests.php:171 -#: includes/Admin/Requests.php:246 -msgid "You do not have permission to access this endpoint." +#: includes/RestAPI/Controllers/Activations.php:121 +msgid "Cannot create existing activation." msgstr "" -#. translators: $1: customer name, $2 customer id, $3: customer email -#: includes/Admin/Requests.php:282 +#: includes/RestAPI/Controllers/Activations.php:293 +#: includes/RestAPI/Controllers/Keys.php:295 +msgid "Import not yet implemented." +msgstr "" + +#: includes/RestAPI/Controllers/Activations.php:306 +#: includes/RestAPI/Controllers/Keys.php:308 +msgid "Export not yet implemented." +msgstr "" + +#. translators: %d is the maximum batch size. +#: includes/RestAPI/Controllers/Controller.php:257 #, php-format -msgid "%1$s (#%2$s - %3$s)" +msgid "Batch limit is %d items." msgstr "" -#: includes/Admin/Settings.php:86 -msgid "General" +#: includes/RestAPI/Controllers/Keys.php:100 +#: includes/RestAPI/Controllers/Keys.php:173 +#: includes/RestAPI/Controllers/Keys.php:211 +#: includes/RestAPI/Controllers/Keys.php:364 +msgid "Key not found." msgstr "" -#: includes/Admin/Settings.php:120 -msgid "General Settings" +#: includes/RestAPI/Controllers/Keys.php:121 +msgid "Cannot create existing key." msgstr "" -#: includes/Admin/Settings.php:122 -msgid "These options determine the behavior and operation of the plugin." +#: includes/RestAPI/Controllers/Software.php:139 +msgid "Invalid product ID." msgstr "" -#: includes/Admin/Settings.php:126 -msgid "Auto-complete orders" +#: includes/RestAPI/Controllers/Software.php:154 +msgid "Serial key is invalid." msgstr "" -#: includes/Admin/Settings.php:128 -msgid "Automatically completes orders after successful payments." +#: includes/RestAPI/Controllers/Software.php:159 +msgid "Serial key is not authorized to use." msgstr "" -#: includes/Admin/Settings.php:133 -msgid "Reuse keys" +#: includes/RestAPI/Controllers/Software.php:165 +msgid "Please complete your order to activate the serial key." msgstr "" -#: includes/Admin/Settings.php:135 -msgid "Recover failed, refunded keys for selling again." +#: includes/RestAPI/Controllers/Software.php:170 +msgid "Serial key is not valid for this product." msgstr "" -#: includes/Admin/Settings.php:136 -msgid "If you enable this option, the keys will be available for selling again if the order is refunded or failed." +#: includes/RestAPI/Controllers/Software.php:175 +msgid "Invalid email address." msgstr "" -#: includes/Admin/Settings.php:142 -msgid "Revoke keys" +#: includes/RestAPI/Controllers/Software.php:180 +msgid "Serial key is expired." msgstr "" -#: includes/Admin/Settings.php:144 -msgid "Revoke keys when the order status changes to cancelled or refunded." +#: includes/RestAPI/Controllers/Software.php:182 +msgid "Serial key is cancelled." msgstr "" -#: includes/Admin/Settings.php:145 -msgid "If you enable this option, the keys will be revoked when the order status changes to cancelled or refunded." +#: includes/RestAPI/Controllers/Software.php:185 +msgid "Invalid serial key." msgstr "" -#: includes/Admin/Settings.php:150 -msgid "Hide keys" +#: includes/RestAPI/Controllers/Software.php:211 +msgid "Serial key is valid." msgstr "" -#: includes/Admin/Settings.php:152 -msgid "Keys will be masked in the list table." +#: includes/RestAPI/Controllers/Software.php:269 +msgid "Instance is already activated." msgstr "" -#: includes/Admin/Settings.php:157 -msgid "Disable software support" +#: includes/RestAPI/Controllers/Software.php:274 +msgid "Activation limit reached." msgstr "" -#: includes/Admin/Settings.php:159 -msgid "Disable Software Licensing support & API functionalities." +#: includes/RestAPI/Controllers/Software.php:290 +msgid "Serial key is activated." msgstr "" -#: includes/Admin/Settings.php:160 -msgid "If you enable this option, the activation menu and it’s functionality will be turned off." +#: includes/RestAPI/Controllers/Software.php:328 +msgid "Instance is missing, You must provide an instance to deactivate license." msgstr "" -#: includes/Admin/Settings.php:166 -msgid "Manage Stocks" +#: includes/RestAPI/Controllers/Software.php:346 +msgid "Instance not found." msgstr "" -#: includes/Admin/Settings.php:168 -msgid "Manage stocks for the key enabled products." +#: includes/RestAPI/Controllers/Software.php:354 +msgid "Serial key is deactivated." msgstr "" -#: includes/Admin/Settings.php:169 -msgid "Enable stock management for key-enabled products. This works only if you select \"Manually Added\" as the key source and enable stock management for the product. Variable product is not supported." +#: includes/Shop.php:48 +msgid "Serial Key Validation" msgstr "" -#: includes/Admin/Settings.php:175 -msgid "WooCommerce PDF Invoices" +#: includes/Shop.php:50 +#: includes/Shop.php:147 +msgid "Serial Key" msgstr "" -#: includes/Admin/Settings.php:177 -msgid "Enable WooCommerce PDF Invoices." +#: includes/Shop.php:51 +#: includes/Shop.php:148 +msgid "Enter your serial key" msgstr "" -#. translators: %s: documentation link -#: includes/Admin/Settings.php:180 -#, php-format -msgid "If you enable this option, the plugin will be compatible with WooCommerce PDF Invoices & Packing Slips plugins and will show the serial keys in the invoice. Check out the documentation for more details." +#: includes/Shop.php:53 +#: includes/Shop.php:114 +#: includes/Shop.php:150 +#: includes/Shop.php:226 +msgid "Enter your email" msgstr "" -#: includes/Admin/Settings.php:191 -msgid "Stock Notification" +#: includes/Shop.php:54 +#: templates/admin/api-validation.php:164 +msgid "Validate" msgstr "" -#: includes/Admin/Settings.php:193 -msgid "These options determine the operation of the key's stock notification." +#: includes/Shop.php:85 +msgid "No products found." msgstr "" -#: includes/Admin/Settings.php:197 -msgid "Stock notification email" +#: includes/Shop.php:99 +#: includes/Shop.php:210 +msgid "Select a product" msgstr "" -#: includes/Admin/Settings.php:199 -msgid "Sends notification emails when key stock is low." +#: includes/Shop.php:145 +msgid "Activate/Deactivate Serial Key" +msgstr "" + +#: includes/Shop.php:152 +msgid "Enter your instance" msgstr "" -#: includes/Admin/Settings.php:205 -msgid "Stock threshold" +#: includes/Shop.php:154 +msgid "Enter platform" msgstr "" -#: includes/Admin/Settings.php:207 -msgid "An email notification will be sent when the key stock falls below the specified number." +#: includes/Shop.php:155 +#: templates/admin/api-actions.php:172 +msgid "Action" msgstr "" -#: includes/Admin/Settings.php:212 -msgid "Notification recipient email" +#: includes/Shop.php:156 +#: templates/admin/api-actions.php:195 +msgid "Submit" msgstr "" -#: includes/Admin/Settings.php:214 -msgid "The email address which will be used to send email notifications." +#: includes/Shop.php:162 +#: templates/admin/api-actions.php:175 +msgid "Activate" msgstr "" -#: includes/Admin/Settings.php:287 -msgid "Create and assign keys for WooCommerce variable products." +#: includes/Shop.php:163 +#: templates/admin/api-actions.php:176 +msgid "Deactivate" msgstr "" -#: includes/Admin/Settings.php:288 -msgid "Generate bulk keys with your custom key generator rule." +#: includes/Shop.php:196 +msgid "Could not find any products with serial numbers enabled." msgstr "" -#: includes/Admin/Settings.php:289 -msgid "Random & sequential key order for the generator rules." +#: includes/Shop.php:278 +#: includes/Shop.php:301 +msgid "Invalid request." msgstr "" -#: includes/Admin/Settings.php:290 -msgid "Automatic key generator to auto-create & assign keys with orders." +#: includes/Shop.php:349 +msgid "Loading" msgstr "" -#: includes/Admin/Settings.php:291 -msgid "License key management option from the order page with required actions." +#: includes/Stocks/ListTable.php:22 +msgid "stock" msgstr "" -#: includes/Admin/Settings.php:292 -msgid "Support for bulk import/export of keys from/to CSV." +#: includes/Stocks/ListTable.php:23 +msgid "stocks" msgstr "" -#: includes/Admin/Settings.php:293 -msgid "Send keys via SMS with Twilio." +#: includes/Stocks/ListTable.php:72 +msgid "No products selling serial keys from \"stock\" found." msgstr "" -#: includes/Admin/Settings.php:294 -msgid "Option to sell keys even if there are no available keys in the stock." +#: includes/Stocks/ListTable.php:98 +msgid "Source" msgstr "" -#: includes/Admin/Settings.php:295 -msgid "Custom deliverable quantity to deliver multiple keys with a single product." +#: includes/Stocks/ListTable.php:174 +msgid "Manual" msgstr "" -#: includes/Admin/Settings.php:296 -msgid "Manual delivery option to manually deliver license keys instead of automatic." +#: includes/Stocks/ListTable.php:176 +msgid "Generator Rule" msgstr "" -#: includes/Admin/Settings.php:297 -msgid "Email template to easily and quickly customize the order confirmation & low stock alert email." +#: includes/Stocks/ListTable.php:178 +msgid "Auto Generated" msgstr "" -#: includes/Admin/Settings.php:298 -msgid "Many more ..." +#: includes/Stocks/ListTable.php:180 +msgid "Unknown" msgstr "" -#: includes/Admin/Settings.php:302 -msgid "Want More?" +#: includes/Stocks/Stocks.php:109 +msgid "Serial Numbers stock running low" msgstr "" -#: includes/Admin/Settings.php:303 -msgid "This plugin offers a premium version which comes with the following features:" +#: includes/template.php:70 +msgid "Active" msgstr "" -#: includes/Admin/Settings.php:309 -msgid "Upgrade to PRO" +#: includes/template.php:307 +msgid "Order is waiting for serial numbers to be assigned." msgstr "" -#: includes/Admin/views/html-api-actions.php:21 +#: templates/admin/api-actions.php:21 msgid "API Actions" msgstr "" -#: includes/Admin/views/html-api-actions.php:26 +#: templates/admin/api-actions.php:26 msgid "You can use the API to perform actions on your website or on another website." msgstr "" -#: includes/Admin/views/html-api-actions.php:29 +#: templates/admin/api-actions.php:29 msgid "To perform an action, you need to send a POST request to the following URL:" msgstr "" -#: includes/Admin/views/html-api-actions.php:33 -#: includes/Admin/views/html-api-validation.php:46 +#: templates/admin/api-actions.php:33 +#: templates/admin/api-validation.php:46 msgid "The request must contain the following parameters:" msgstr "" -#: includes/Admin/views/html-api-actions.php:37 -#: includes/Admin/views/html-api-actions.php:104 -#: includes/Admin/views/html-api-validation.php:50 -#: includes/Admin/views/html-api-validation.php:105 +#: templates/admin/api-actions.php:37 +#: templates/admin/api-actions.php:104 +#: templates/admin/api-validation.php:50 +#: templates/admin/api-validation.php:105 msgid "The ID of the product for which the serial key is valid." msgstr "" -#: includes/Admin/views/html-api-actions.php:40 -#: includes/Admin/views/html-api-validation.php:53 +#: templates/admin/api-actions.php:40 +#: templates/admin/api-validation.php:53 msgid "The serial key to validate." msgstr "" -#: includes/Admin/views/html-api-actions.php:43 +#: templates/admin/api-actions.php:43 msgid "The request type. Must be set to \"activate\" or \"deactivate\"." msgstr "" -#: includes/Admin/views/html-api-actions.php:46 +#: templates/admin/api-actions.php:46 msgid "Instance is the base of activation and deactivation. It is a unique identifier for the installation. For example, you can use the domain name of the website." msgstr "" -#: includes/Admin/views/html-api-actions.php:49 +#: templates/admin/api-actions.php:49 msgid "Optional. The platform on which the serial key is used. For example, \"Windows\" or \"Mac\"." msgstr "" -#: includes/Admin/views/html-api-actions.php:52 -#: includes/Admin/views/html-api-validation.php:59 +#: templates/admin/api-actions.php:52 +#: templates/admin/api-validation.php:59 msgid "Using email is completely voluntary. The API will verify that the serial number is associated with the given email address." msgstr "" -#: includes/Admin/views/html-api-actions.php:57 -#: includes/Admin/views/html-api-validation.php:63 +#: templates/admin/api-actions.php:57 +#: templates/admin/api-validation.php:63 msgid "Example:" msgstr "" -#: includes/Admin/views/html-api-actions.php:75 -#: includes/Admin/views/html-api-validation.php:80 +#: templates/admin/api-actions.php:75 +#: templates/admin/api-validation.php:80 msgid "The API will return a JSON response with the following parameters:" msgstr "" -#: includes/Admin/views/html-api-actions.php:80 +#: templates/admin/api-actions.php:80 msgid "The response code. \"key_activated\" or \"key_deactivated\" if the request was successful. \"invalid_key\" or \"invalid_request\" if the request was not successful." msgstr "" -#: includes/Admin/views/html-api-actions.php:83 -#: includes/Admin/views/html-api-validation.php:87 +#: templates/admin/api-actions.php:83 +#: templates/admin/api-validation.php:87 msgid "The response message. If the serial key is valid, the message will be \"Serial key is valid\"." msgstr "" -#: includes/Admin/views/html-api-actions.php:86 +#: templates/admin/api-actions.php:86 msgid "Activated or deactivated when the request is successful." msgstr "" -#: includes/Admin/views/html-api-actions.php:89 -#: includes/Admin/views/html-api-validation.php:90 +#: templates/admin/api-actions.php:89 +#: templates/admin/api-validation.php:90 msgid "The activation limit for the serial key." msgstr "" -#: includes/Admin/views/html-api-actions.php:92 -#: includes/Admin/views/html-api-validation.php:93 +#: templates/admin/api-actions.php:92 +#: templates/admin/api-validation.php:93 msgid "The number of activations for the serial key." msgstr "" -#: includes/Admin/views/html-api-actions.php:95 -#: includes/Admin/views/html-api-validation.php:96 +#: templates/admin/api-actions.php:95 +#: templates/admin/api-validation.php:96 msgid "The number of activations left for the serial key." msgstr "" -#: includes/Admin/views/html-api-actions.php:98 -#: includes/Admin/views/html-api-validation.php:99 +#: templates/admin/api-actions.php:98 +#: templates/admin/api-validation.php:99 msgid "The expiration date for the serial key." msgstr "" -#: includes/Admin/views/html-api-actions.php:101 +#: templates/admin/api-actions.php:101 msgid "The expiration date for the serial key in Unix timestamp format." msgstr "" -#: includes/Admin/views/html-api-actions.php:107 -#: includes/Admin/views/html-api-validation.php:108 +#: templates/admin/api-actions.php:107 +#: templates/admin/api-validation.php:108 msgid "The name of the product for which the serial key is valid." msgstr "" -#: includes/Admin/views/html-api-actions.php:110 -#: includes/Admin/views/html-api-validation.php:111 +#: templates/admin/api-actions.php:110 +#: templates/admin/api-validation.php:111 msgid "The list of activations for the serial key." msgstr "" -#: includes/Admin/views/html-api-actions.php:113 +#: templates/admin/api-actions.php:113 msgid "You can use the form below to test the API activation/deactivation." msgstr "" -#: includes/Admin/views/html-api-actions.php:126 +#: templates/admin/api-actions.php:126 msgid "Select a product to activate/deactivate serial key for." msgstr "" -#: includes/Admin/views/html-api-actions.php:133 +#: templates/admin/api-actions.php:133 msgid "Please enter serial key to activate/deactivate" msgstr "" -#: includes/Admin/views/html-api-actions.php:135 +#: templates/admin/api-actions.php:135 msgid "Required field. Enter serial key to activate/deactivate." msgstr "" -#: includes/Admin/views/html-api-actions.php:143 +#: templates/admin/api-actions.php:143 msgid "Please enter a unique instance" msgstr "" -#: includes/Admin/views/html-api-actions.php:145 +#: templates/admin/api-actions.php:145 msgid "Required field. Instance is the unique identifier of the activation record. It is used to identify the activation when activating/deactivating serial key." msgstr "" -#: includes/Admin/views/html-api-actions.php:154 +#: templates/admin/api-actions.php:154 msgid "Please enter a platform. e.g. Windows" msgstr "" -#: includes/Admin/views/html-api-actions.php:156 +#: templates/admin/api-actions.php:156 msgid "Optional field. Platform is the extra information of the activation record. You can use it to identify the platform of the activation." msgstr "" -#: includes/Admin/views/html-api-actions.php:162 -#: includes/Admin/views/html-api-validation.php:144 -#: includes/Admin/views/html-edit-key.php:154 -#: includes/deprecated.php:361 -#: includes/Frontend/Shortcodes.php:49 -#: includes/Frontend/Shortcodes.php:146 -msgid "Email" -msgstr "" - -#: includes/Admin/views/html-api-actions.php:164 -#: includes/Admin/views/html-api-validation.php:146 +#: templates/admin/api-actions.php:164 +#: templates/admin/api-validation.php:146 msgid "Please enter a valid email address" msgstr "" -#: includes/Admin/views/html-api-actions.php:166 +#: templates/admin/api-actions.php:166 msgid "Optional field when duplicate key is off. If email is provided, only serial key that are assigned to the email will be activated/deactivated otherwise ignored." msgstr "" -#: includes/Admin/views/html-api-actions.php:172 -#: includes/Frontend/Shortcodes.php:152 -msgid "Action" -msgstr "" - -#: includes/Admin/views/html-api-actions.php:175 -#: includes/Frontend/Shortcodes.php:159 -msgid "Activate" -msgstr "" - -#: includes/Admin/views/html-api-actions.php:176 -#: includes/Frontend/Shortcodes.php:160 -msgid "Deactivate" -msgstr "" - -#: includes/Admin/views/html-api-actions.php:179 +#: templates/admin/api-actions.php:179 msgid "Select an action to perform on the serial key." msgstr "" -#: includes/Admin/views/html-api-actions.php:185 -#: includes/Admin/views/html-api-validation.php:153 +#: templates/admin/api-actions.php:185 +#: templates/admin/api-validation.php:153 msgid "API response" msgstr "" -#: includes/Admin/views/html-api-actions.php:195 -#: includes/Frontend/Shortcodes.php:153 -msgid "Submit" -msgstr "" - -#: includes/Admin/views/html-api-validation.php:20 +#: templates/admin/api-validation.php:20 msgid "API Validation" msgstr "" #. translators: %s: link to the pro version -#: includes/Admin/views/html-api-validation.php:30 +#: templates/admin/api-validation.php:30 #, php-format msgid "You are using the free version of Serial Numbers for WooCommerce. Upgrade to Pro to get more features." msgstr "" -#: includes/Admin/views/html-api-validation.php:39 +#: templates/admin/api-validation.php:39 msgid "You can use the API to validate serial keys on your website or on another website." msgstr "" -#: includes/Admin/views/html-api-validation.php:42 +#: templates/admin/api-validation.php:42 msgid "To validate a serial key, you need to send a GET request to the following URL:" msgstr "" -#: includes/Admin/views/html-api-validation.php:56 +#: templates/admin/api-validation.php:56 msgid "The request type. Must be set to \"validate\"." msgstr "" -#: includes/Admin/views/html-api-validation.php:84 +#: templates/admin/api-validation.php:84 msgid "The response code. If the serial key is valid, the code will be \"key_valid\"." msgstr "" -#: includes/Admin/views/html-api-validation.php:102 +#: templates/admin/api-validation.php:102 msgid "The status of the serial key." msgstr "" -#: includes/Admin/views/html-api-validation.php:115 +#: templates/admin/api-validation.php:115 msgid "You can test the API using the form below." msgstr "" -#: includes/Admin/views/html-api-validation.php:129 +#: templates/admin/api-validation.php:129 msgid "Select a product to validate serial key for." msgstr "" -#: includes/Admin/views/html-api-validation.php:136 +#: templates/admin/api-validation.php:136 msgid "Please enter serial key to validate" msgstr "" -#: includes/Admin/views/html-api-validation.php:138 +#: templates/admin/api-validation.php:138 msgid "Required field. Enter serial key to validate." msgstr "" -#: includes/Admin/views/html-api-validation.php:148 +#: templates/admin/api-validation.php:148 msgid "Optional field. If email is provided, only serial key that are assigned to the email will be validated otherwise ignored." msgstr "" -#: includes/Admin/views/html-api-validation.php:164 -#: includes/Frontend/Shortcodes.php:51 -msgid "Validate" -msgstr "" - -#: includes/Admin/views/html-edit-key.php:16 +#: templates/admin/edit-key.php:16 msgid "Edit Serial Key" msgstr "" -#: includes/Admin/views/html-edit-key.php:18 +#: templates/admin/edit-key.php:18 msgid "Add Another" msgstr "" -#: includes/Admin/views/html-edit-key.php:22 +#: templates/admin/edit-key.php:22 msgid "Go Back" msgstr "" -#: includes/Admin/views/html-edit-key.php:31 +#: templates/admin/edit-key.php:31 msgid "Key Details" msgstr "" -#: includes/Admin/views/html-edit-key.php:40 +#: templates/admin/edit-key.php:40 msgid "Select Product" msgstr "" -#: includes/Admin/views/html-edit-key.php:50 +#: templates/admin/edit-key.php:50 msgid "Select the product for which this key is applicable." msgstr "" -#: includes/Admin/views/html-edit-key.php:56 +#: templates/admin/edit-key.php:56 msgid "Serial key" msgstr "" -#: includes/Admin/views/html-edit-key.php:61 +#: templates/admin/edit-key.php:61 msgid "Enter your serial key, also supports multiline. For example: 4CE0460D0G-4CE0460D1G-4CE0460D2G" msgstr "" -#: includes/Admin/views/html-edit-key.php:71 +#: templates/admin/edit-key.php:71 msgid "Maximum number of times the key can be used to activate the software. If the product is not software, keep it blank." msgstr "" -#: includes/Admin/views/html-edit-key.php:77 +#: templates/admin/edit-key.php:77 msgid "Valid for" msgstr "" -#: includes/Admin/views/html-edit-key.php:82 +#: templates/admin/edit-key.php:82 msgid "Days" msgstr "" -#: includes/Admin/views/html-edit-key.php:85 +#: templates/admin/edit-key.php:85 msgid "Number of days the key will be valid from the purchase date. Leave it blank for lifetime validity." msgstr "" -#: includes/Admin/views/html-edit-key.php:99 +#: templates/admin/edit-key.php:99 msgid "Serial key status auto-updates with order status. Avoid manual changes." msgstr "" -#: includes/Admin/views/html-edit-key.php:104 +#: templates/admin/edit-key.php:104 msgid "Order ID" msgstr "" -#: includes/Admin/views/html-edit-key.php:106 +#: templates/admin/edit-key.php:106 msgid "Select Order" msgstr "" -#: includes/Admin/views/html-edit-key.php:115 +#: templates/admin/edit-key.php:115 msgid "The order to which the serial number will be assigned." msgstr "" -#: includes/Admin/views/html-edit-key.php:126 +#: templates/admin/edit-key.php:126 msgid "Actions" msgstr "" -#: includes/Admin/views/html-edit-key.php:132 +#: templates/admin/edit-key.php:132 msgid "Save Key" msgstr "" -#: includes/Admin/views/html-edit-key.php:139 +#: templates/admin/edit-key.php:139 msgid "Customer details" msgstr "" -#: includes/Admin/views/html-edit-key.php:146 +#: templates/admin/edit-key.php:146 msgid "Name" msgstr "" -#: includes/Admin/views/html-edit-key.php:162 +#: templates/admin/edit-key.php:162 msgid "Address" msgstr "" -#: includes/Admin/views/html-edit-key.php:171 +#: templates/admin/edit-key.php:171 msgid "Phone" msgstr "" -#: includes/Admin/views/html-edit-key.php:181 +#: templates/admin/edit-key.php:181 msgid "View Order" msgstr "" -#: includes/Admin/views/html-list-activations.php:26 +#: templates/admin/list-activations.php:26 msgid "Search activation" msgstr "" -#: includes/Admin/views/html-list-keys.php:21 +#: templates/admin/list-keys.php:21 msgid "Add New" msgstr "" -#: includes/Admin/views/html-list-keys.php:33 +#: templates/admin/list-keys.php:33 msgid "Generate" msgstr "" -#: includes/Admin/views/html-list-keys.php:44 +#: templates/admin/list-keys.php:44 msgid "Search key" msgstr "" -#: includes/Admin/views/html-list-stock.php:16 +#: templates/admin/list-stock.php:16 msgid "Search" msgstr "" #. translators: 1: plugin name 2: WooCommerce -#: includes/Admin/views/html-notice-dependency.php:13 +#: templates/admin/notices/dependency.php:13 #, php-format msgid "%1$s requires %2$s to be installed and active." msgstr "" -#: includes/Admin/views/html-notice-dependency.php:15 +#: templates/admin/notices/dependency.php:15 msgid "WooCommerce" msgstr "" -#: includes/API.php:80 -msgid "Missing data." -msgstr "" - -#. translators: %1$s: product title, %2$s: stock quantity. -#: includes/Cart.php:58 -#, php-format -msgid "Sorry, there aren’t enough Serial Keys for %1$s. Please remove this item or lower the quantity. For now, we have %2$s Serial Keys for this product." -msgstr "" - -#: includes/Compat.php:103 -msgid "Activation count" -msgstr "" - -#: includes/Compat.php:107 -msgid "Activation email" -msgstr "" - -#: includes/Cron.php:59 -msgid "Serial Numbers stock running low" -msgstr "" - -#: includes/deprecated.php:154 -msgid "Serial number not found." -msgstr "" - -#: includes/deprecated.php:360 -msgid "Serial Number" -msgstr "" - -#: includes/deprecated.php:362 -#: includes/functions.php:1025 -#: includes/Functions/Template.php:52 -msgid "Activation Limit" -msgstr "" - -#: includes/deprecated.php:363 -msgid "Expires" -msgstr "" - -#: includes/Frontend/Frontend.php:55 -msgid "Loading" -msgstr "" - -#: includes/Frontend/Shortcodes.php:45 -msgid "Serial Key Validation" -msgstr "" - -#: includes/Frontend/Shortcodes.php:47 -#: includes/Frontend/Shortcodes.php:144 -msgid "Serial Key" -msgstr "" - -#: includes/Frontend/Shortcodes.php:48 -#: includes/Frontend/Shortcodes.php:145 -msgid "Enter your serial key" -msgstr "" - -#: includes/Frontend/Shortcodes.php:50 -#: includes/Frontend/Shortcodes.php:111 -#: includes/Frontend/Shortcodes.php:147 -#: includes/Frontend/Shortcodes.php:223 -msgid "Enter your email" -msgstr "" - -#: includes/Frontend/Shortcodes.php:82 -msgid "No products found." -msgstr "" - -#: includes/Frontend/Shortcodes.php:96 -#: includes/Frontend/Shortcodes.php:207 -msgid "Select a product" -msgstr "" - -#: includes/Frontend/Shortcodes.php:142 -msgid "Activate/Deactivate Serial Key" -msgstr "" - -#: includes/Frontend/Shortcodes.php:149 -msgid "Enter your instance" -msgstr "" - -#: includes/Frontend/Shortcodes.php:151 -msgid "Enter platform" -msgstr "" - -#: includes/Frontend/Shortcodes.php:193 -msgid "Could not find any products with serial numbers enabled." -msgstr "" - -#: includes/Frontend/Shortcodes.php:275 -#: includes/Frontend/Shortcodes.php:298 -msgid "Invalid request." -msgstr "" - -#: includes/functions.php:91 -msgid "Manually added" -msgstr "" - -#. translators: 1: product title 2: source and 3: Quantity -#: includes/functions.php:539 -#, php-format -msgid "There is not enough serial numbers for the product %1$s from selected source %2$s, needed total %3$d." -msgstr "" - -#: includes/functions.php:1020 -#: includes/Functions/Template.php:47 -msgid "Activation Email" -msgstr "" - -#: includes/functions.php:1026 -#: includes/functions.php:1031 -#: includes/Functions/Template.php:53 -#: includes/Functions/Template.php:58 -msgid "None" -msgstr "" - -#: includes/functions.php:1030 -#: includes/Functions/Template.php:57 -msgid "Activation Count" -msgstr "" - -#: includes/functions.php:1035 -#: includes/Functions/Template.php:62 -msgid "Expire Date" -msgstr "" - -#: includes/Functions/Template.php:70 -msgid "Active" -msgstr "" - -#: includes/Functions/Template.php:307 -msgid "Order is waiting for serial numbers to be assigned." -msgstr "" - -#: includes/Installer.php:60 -msgid "Once a Minute" -msgstr "" - -#: includes/Models/Activation.php:245 -msgid "Serial id is required." -msgstr "" - -#: includes/Models/Activation.php:250 -msgid "Instance is required." -msgstr "" - -#: includes/Models/Key.php:509 -msgid "Product id is required." -msgstr "" - -#: includes/Models/Key.php:514 -msgid "Product id is invalid." -msgstr "" - -#: includes/Models/Key.php:519 -#: includes/RestAPI.php:152 -msgid "Serial key is required." -msgstr "" - -#: includes/Models/Key.php:531 -msgid "Serial key already exists. Duplicate serial keys are not allowed." -msgstr "" - -#: includes/Models/Key.php:537 -msgid "Order id is invalid." -msgstr "" - -#: includes/Orders.php:82 -msgid "Order automatically completed by the Serial Numbers for WooCommerce." -msgstr "" - -#: includes/RestAPI.php:148 -msgid "Invalid product ID." -msgstr "" - -#: includes/RestAPI.php:163 -msgid "Serial key is invalid." -msgstr "" - -#: includes/RestAPI.php:168 -msgid "Serial key is not authorized to use." -msgstr "" - -#: includes/RestAPI.php:174 -msgid "Please complete your order to activate the serial key." -msgstr "" - -#: includes/RestAPI.php:179 -msgid "Serial key is not valid for this product." -msgstr "" - -#: includes/RestAPI.php:184 -msgid "Invalid email address." -msgstr "" - -#: includes/RestAPI.php:189 -msgid "Serial key is expired." -msgstr "" - -#: includes/RestAPI.php:191 -msgid "Serial key is cancelled." -msgstr "" - -#: includes/RestAPI.php:194 -msgid "Invalid serial key." -msgstr "" - -#: includes/RestAPI.php:220 -msgid "Serial key is valid." -msgstr "" - -#: includes/RestAPI.php:278 -msgid "Instance is already activated." -msgstr "" - -#: includes/RestAPI.php:283 -msgid "Activation limit reached." -msgstr "" - -#: includes/RestAPI.php:299 -msgid "Serial key is activated." -msgstr "" - -#: includes/RestAPI.php:337 -msgid "Instance is missing, You must provide an instance to deactivate license." -msgstr "" - -#: includes/RestAPI.php:355 -msgid "Instance not found." -msgstr "" - -#: includes/RestAPI.php:363 -msgid "Serial key is deactivated." -msgstr "" - #: templates/email-stock-notification.php:14 msgid "Hi There," msgstr "" diff --git a/templates/admin/api-actions.php b/templates/admin/api-actions.php index c84d7433..bfd7f3a5 100644 --- a/templates/admin/api-actions.php +++ b/templates/admin/api-actions.php @@ -2,7 +2,7 @@ /** * API actions * - * @package WooCommerceSerialNumbers/Admin/Views + * @package PluginEver\SerialNumbers/Admin/Views * @version 1.4.6 */ diff --git a/templates/admin/api-validation.php b/templates/admin/api-validation.php index 4013fc26..15753e73 100644 --- a/templates/admin/api-validation.php +++ b/templates/admin/api-validation.php @@ -2,7 +2,7 @@ /** * API Validation * - * @package WooCommerceSerialNumbers/Admin/Views + * @package PluginEver\SerialNumbers/Admin/Views * @version 1.4.6 */ diff --git a/templates/admin/edit-key.php b/templates/admin/edit-key.php index 47da7852..fc974dbc 100644 --- a/templates/admin/edit-key.php +++ b/templates/admin/edit-key.php @@ -2,9 +2,9 @@ /** * The template for editing a key. * - * @package WooCommerceSerialNumbers/Admin/Views + * @package PluginEver\SerialNumbers/Admin/Views * @version 1.4.6 - * @var \WooCommerceSerialNumbers\Models\Key $key + * @var \PluginEver\SerialNumbers\Models\Key $key */ defined( 'ABSPATH' ) || exit; diff --git a/templates/admin/list-activations.php b/templates/admin/list-activations.php index 61038afc..7441c918 100644 --- a/templates/admin/list-activations.php +++ b/templates/admin/list-activations.php @@ -3,12 +3,12 @@ * List Activations. * * @since 1.0.0 - * @package WooCommerceSerialNumbers\Admin\Views + * @package PluginEver\SerialNumbers\Admin\Views */ defined( 'ABSPATH' ) || exit; -$list_table = new WooCommerceSerialNumbers\Activations\ListTable(); +$list_table = new PluginEver\SerialNumbers\Activations\ListTable(); $doaction = $list_table->current_action(); $list_table->process_bulk_actions( $doaction ); ?> diff --git a/templates/admin/list-keys.php b/templates/admin/list-keys.php index 981a856e..73c2d4cd 100644 --- a/templates/admin/list-keys.php +++ b/templates/admin/list-keys.php @@ -3,12 +3,12 @@ * List keys. * * @since 1.0.0 - * @package WooCommerceSerialNumbers\Admin\Views + * @package PluginEver\SerialNumbers\Admin\Views */ defined( 'ABSPATH' ) || exit; -$list_table = new WooCommerceSerialNumbers\Keys\ListTable(); +$list_table = new PluginEver\SerialNumbers\Keys\ListTable(); $doaction = $list_table->current_action(); $list_table->process_bulk_actions( $doaction ); ?> diff --git a/templates/admin/list-stock.php b/templates/admin/list-stock.php index 13f25f1c..97b3f305 100644 --- a/templates/admin/list-stock.php +++ b/templates/admin/list-stock.php @@ -2,11 +2,11 @@ /** * List all stocks * - * @package WooCommerceSerialNumbers/Admin/Views + * @package PluginEver\SerialNumbers/Admin/Views */ defined( 'ABSPATH' ) || exit; -$list_table = new WooCommerceSerialNumbers\Stocks\ListTable(); +$list_table = new PluginEver\SerialNumbers\Stocks\ListTable(); ?> diff --git a/templates/admin/notices/dependency.php b/templates/admin/notices/dependency.php index ec32ff8a..7f59a8e2 100644 --- a/templates/admin/notices/dependency.php +++ b/templates/admin/notices/dependency.php @@ -3,7 +3,7 @@ * Missing dependencies notice. * * @since 2.4.0 - * @package WooCommerceSerialNumbers\Admin\Views + * @package PluginEver\SerialNumbers\Admin\Views */ defined( 'ABSPATH' ) || exit; diff --git a/templates/admin/reports.php b/templates/admin/reports.php index 23365141..ef21f886 100644 --- a/templates/admin/reports.php +++ b/templates/admin/reports.php @@ -3,7 +3,7 @@ * Show the reports page. * * @since 1.0.0 - * @package WooCommerceSerialNumbers\Admin\Views + * @package PluginEver\SerialNumbers\Admin\Views */ defined( 'ABSPATH' ) || exit; diff --git a/templates/admin/tools.php b/templates/admin/tools.php index fe4d7704..1ac26336 100644 --- a/templates/admin/tools.php +++ b/templates/admin/tools.php @@ -3,7 +3,7 @@ * Show the tools page. * * @since 1.0.0 - * @package WooCommerceSerialNumbers\Admin\Views + * @package PluginEver\SerialNumbers\Admin\Views */ defined( 'ABSPATH' ) || exit; diff --git a/templates/email-stock-notification.php b/templates/email-stock-notification.php index 2a9ae017..f3fad3bd 100644 --- a/templates/email-stock-notification.php +++ b/templates/email-stock-notification.php @@ -3,7 +3,7 @@ * Low stock email notification template. * * @since 1.2.0 - * @package WooCommerceSerialNumbers + * @package PluginEver\SerialNumbers * @var array $low_stock_products List of low stock products. */ diff --git a/tests/bootstrap.php b/tests/bootstrap.php index d25bbac1..b594d11d 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,7 +1,7 @@ assertTrue( class_exists( \WooCommerceSerialNumbers\Models\Key::class ) ); + $this->assertTrue( class_exists( \PluginEver\SerialNumbers\Models\Key::class ) ); // The list table is admin-only and extends WP_List_Table. if ( ! class_exists( '\WP_List_Table' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } - $this->assertTrue( class_exists( \WooCommerceSerialNumbers\Admin\ListTables\ListTable::class ) ); + $this->assertTrue( class_exists( \PluginEver\SerialNumbers\Admin\ListTables\ListTable::class ) ); } /** @@ -79,4 +79,60 @@ public function testHelperDefaults(): void { public function testSingletonInstance(): void { $this->assertSame( WCSN(), WCSN() ); } + + /** + * The pre-rename class names resolve through the alias loader. + */ + public function testLegacyClassAliases(): void { + if ( ! class_exists( '\WP_List_Table' ) ) { + require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; + } + + $aliases = array( + 'WooCommerceSerialNumbers\\Plugin' => \PluginEver\SerialNumbers\Plugin::class, + 'WooCommerceSerialNumbers\\Models\\Key' => \PluginEver\SerialNumbers\Models\Key::class, + 'WooCommerceSerialNumbers\\Models\\Activation' => \PluginEver\SerialNumbers\Models\Activation::class, + 'WooCommerceSerialNumbers\\Admin\\Menus' => \PluginEver\SerialNumbers\Admin\Menus::class, + 'WooCommerceSerialNumbers\\Admin\\Settings' => \PluginEver\SerialNumbers\Admin\Settings::class, + 'WooCommerceSerialNumbers\\Admin\\ListTables\\ListTable' => \PluginEver\SerialNumbers\Admin\ListTables\ListTable::class, + 'WooCommerceSerialNumbers\\Admin\\ListTables\\KeysTable' => \PluginEver\SerialNumbers\Keys\ListTable::class, + 'WooCommerceSerialNumbers\\Admin\\ListTables\\ActivationsTable' => \PluginEver\SerialNumbers\Activations\ListTable::class, + 'WooCommerceSerialNumbers\\Admin\\ListTables\\StockTable' => \PluginEver\SerialNumbers\Stocks\ListTable::class, + ); + + foreach ( $aliases as $legacy => $current ) { + $this->assertTrue( class_exists( $legacy ), "Legacy class {$legacy} should resolve." ); + $this->assertInstanceOf( $current, ( new \ReflectionClass( $legacy ) )->newInstanceWithoutConstructor() ); + } + } + + /** + * The tools tabs are registered under the pre-rename callable identity the pro removes by. + */ + public function testLegacyToolsTabCallbacks(): void { + // The admin components don't boot in the test context, so register the menus directly. + WCSN()->make( \PluginEver\SerialNumbers\Admin\Menus::class )->register(); + + foreach ( array( 'import', 'export', 'generators' ) as $tab ) { + $this->assertNotFalse( + has_action( "wc_serial_numbers_tools_tab_{$tab}", "WooCommerceSerialNumbers\\Admin\\Menus::{$tab}_tab" ), + "Tools tab {$tab} should keep its legacy callback identity." + ); + } + } + + /** + * The REST API exposes the legacy software routes and the new resource routes. + */ + public function testRestRoutes(): void { + do_action( 'rest_api_init' ); + $routes = rest_get_server()->get_routes(); + + foreach ( array( '/wcsn/validate', '/wcsn/activate', '/wcsn/deactivate' ) as $route ) { + $this->assertArrayHasKey( $route, $routes, "Legacy route {$route} should exist." ); + } + + $this->assertArrayHasKey( '/wcsn/v1/keys', $routes ); + $this->assertArrayHasKey( '/wcsn/v1/activations', $routes ); + } } diff --git a/tests/phpunit/EncryptionTest.php b/tests/phpunit/EncryptionTest.php index af9b8d1c..0f650860 100644 --- a/tests/phpunit/EncryptionTest.php +++ b/tests/phpunit/EncryptionTest.php @@ -1,9 +1,9 @@ * @copyright 2026 ByteEver * @license GPL-2.0+ - * @package WooCommerceSerialNumbers + * @package PluginEver\SerialNumbers */ -use WooCommerceSerialNumbers\Installer; -use WooCommerceSerialNumbers\Plugin; +use PluginEver\SerialNumbers\Installer; +use PluginEver\SerialNumbers\Plugin; defined( 'ABSPATH' ) || exit; From 2c4c322aa15a8da9ed82131ada1bc28423690fde Mon Sep 17 00:00:00 2001 From: sultann Date: Thu, 11 Jun 2026 02:04:59 +0600 Subject: [PATCH 5/5] Remove the legacy model compatibility layer in favor of the native B8 models API --- composer.json | 4 +- includes/Activations/Activations.php | 2 +- includes/Activations/ListTable.php | 30 +- includes/Keys/Admin.php | 4 +- includes/Keys/Keys.php | 10 +- includes/Keys/ListTable.php | 33 +- includes/Models/Activation.php | 41 +- includes/Models/Key.php | 128 +++---- includes/Models/Model.php | 442 +--------------------- includes/RestAPI/Controllers/Software.php | 44 +-- includes/functions.php | 81 +++- tests/phpunit/KeyModelTest.php | 6 +- tests/phpunit/OrderDeliveryTest.php | 6 +- 13 files changed, 200 insertions(+), 631 deletions(-) diff --git a/composer.json b/composer.json index 05af48f8..b087386d 100644 --- a/composer.json +++ b/composer.json @@ -48,10 +48,8 @@ "strauss": [ "test -f /tmp/strauss.phar || curl -o /tmp/strauss.phar -L https://github.com/BrianHenryIE/strauss/releases/download/0.24.1/strauss.phar", "@php /tmp/strauss.phar", - "@patch-models", "@composer dump-autoload --no-scripts" - ], - "patch-models": "php -r 'foreach (array_merge(glob(\"vendor/byteever/models/src/*.php\"), glob(\"vendor/byteever/models/src/Traits/*.php\"), glob(\"vendor/byteever/models/src/Relations/*.php\")) as $f) { $c = file_get_contents($f); $c = preg_replace(\"/->get\\\\((?!\\\\))/\", \"->get_attribute(\", $c); $c = str_replace(\"function get(string \", \"function get_attribute(string \", $c); file_put_contents($f, $c); }'" + ] }, "extra": { "strauss": { diff --git a/includes/Activations/Activations.php b/includes/Activations/Activations.php index 2e23fbaf..788f3408 100644 --- a/includes/Activations/Activations.php +++ b/includes/Activations/Activations.php @@ -45,7 +45,7 @@ public function register(): void { * @since 1.0.0 */ public function update_activation_count( $activation ) { - $key = Key::get( $activation->get_serial_id() ); + $key = Key::find( $activation->get_serial_id() ); if ( $key ) { $key->recount_remaining_activation(); } diff --git a/includes/Activations/ListTable.php b/includes/Activations/ListTable.php index 543c11d6..368eca68 100644 --- a/includes/Activations/ListTable.php +++ b/includes/Activations/ListTable.php @@ -62,29 +62,23 @@ public function prepare_items() { $search = filter_input( INPUT_GET, 's', FILTER_SANITIZE_SPECIAL_CHARS ); $product_id = filter_input( INPUT_GET, 'product_id', FILTER_SANITIZE_NUMBER_INT ); $order_id = filter_input( INPUT_GET, 'order_id', FILTER_SANITIZE_NUMBER_INT ); - $customer_id = filter_input( INPUT_GET, 'customer_id', FILTER_SANITIZE_NUMBER_INT ); $id = filter_input( INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT ); $serial_id = filter_input( INPUT_GET, 'serial_id', FILTER_SANITIZE_NUMBER_INT ); - if ( array_key_exists( $orderby, $this->get_sortable_columns() ) && 'order_date' !== $orderby ) { - $args['orderby'] = $orderby; - } - $args = array( - 'per_page' => $per_page, - 'paged' => $current_page, - 'orderby' => $orderby, - 'order' => $order, - 'product_id' => $product_id, - 'order_id' => $order_id, - 'customer_id' => $customer_id, - 'include' => $id, - 'search' => $search, - 'serial_id' => $serial_id, + 'limit' => $per_page, + 'page' => $current_page, + 'orderby' => $orderby, + 'order' => $order, + 'product_id' => $product_id, + 'order_id' => $order_id, + 'include' => $id, + 'search' => $search, + 'serial_id' => $serial_id, ); - $this->items = Activation::query( $args ); - $this->total_count = Activation::count( $args ); + $this->items = wcsn_get_activations( $args ); + $this->total_count = wcsn_get_activations( $args, true ); $this->set_pagination_args( array( @@ -136,7 +130,7 @@ public function process_bulk_actions( $doaction ) { } foreach ( $ids as $id ) { // Check the permissions on each. - $key = Activation::get( $id ); + $key = Activation::find( $id ); if ( ! $key ) { continue; } diff --git a/includes/Keys/Admin.php b/includes/Keys/Admin.php index 8359dad9..fe566813 100644 --- a/includes/Keys/Admin.php +++ b/includes/Keys/Admin.php @@ -51,8 +51,8 @@ public function output_page() { $add = isset( $_GET['add'] ) ? true : false; $edit = isset( $_GET['edit'] ) ? absint( $_GET['edit'] ) : 0; if ( $edit ) { - $key = new Key( $edit ); - if ( ! $key->exists() ) { + $key = Key::find( $edit ); + if ( ! $key ) { wp_safe_redirect( remove_query_arg( 'edit' ) ); exit(); } diff --git a/includes/Keys/Keys.php b/includes/Keys/Keys.php index 42e7c270..9b3ee52f 100644 --- a/includes/Keys/Keys.php +++ b/includes/Keys/Keys.php @@ -32,7 +32,7 @@ class Keys extends Component { * @return void */ public function register(): void { - add_action( 'wc_serial_numbers_key_insert', array( $this, 'enable_product' ) ); + add_action( 'wc_serial_numbers_key_inserted', array( $this, 'enable_product' ) ); add_action( 'wc_serial_numbers_key_deleted', array( $this, 'delete_activations' ) ); add_action( 'wc_serial_numbers_key_saved', array( $this, 'clear_order_keys_cache' ) ); add_action( 'wc_serial_numbers_key_deleted', array( $this, 'clear_order_keys_cache' ) ); @@ -65,14 +65,12 @@ public function expire_outdated_serials() { /** * Enable product. * - * @param int $key_id The key ID. + * @param Key $key The key object. * * @since 1.4.6 */ - public function enable_product( $key_id ) { - $key = Key::get( $key_id ); - - if ( $key ) { + public function enable_product( $key ) { + if ( $key instanceof Key ) { $product_id = $key->get_product_id(); if ( $product_id ) { diff --git a/includes/Keys/ListTable.php b/includes/Keys/ListTable.php index 6aa0b701..b90904e2 100644 --- a/includes/Keys/ListTable.php +++ b/includes/Keys/ListTable.php @@ -114,18 +114,29 @@ public function prepare_items() { } $args = array( - 'per_page' => $per_page, - 'paged' => $current_page, - 'orderby' => $orderby, - 'order' => $order, - 'status' => $status, - 'product_id' => $product_id, - 'order_id' => $order_id, - 'customer_id' => $customer_id, - 'include' => $id, - 'search' => $search, + 'limit' => $per_page, + 'page' => $current_page, + 'orderby' => $orderby, + 'order' => $order, + 'status' => $status, + 'product_id' => $product_id, + 'order_id' => $order_id, + 'include' => $id, + 'search' => $search, ); + // Constrain to the orders of the given customer. + if ( ! empty( $customer_id ) ) { + $order_ids = wc_get_orders( + array( + 'customer_id' => absint( $customer_id ), + 'limit' => - 1, + 'return' => 'ids', + ) + ); + $args['order_id__in'] = ! empty( $order_ids ) ? $order_ids : array( 0 ); + } + $this->items = Key::query( $args ); $this->available_count = Key::count( array_merge( $args, array( 'status' => 'available' ) ) ); $this->pending_count = Key::count( array_merge( $args, array( 'status' => 'pending' ) ) ); @@ -305,7 +316,7 @@ public function process_bulk_actions( $doaction ) { } foreach ( $ids as $id ) { // Check the permissions on each. - $key = Key::get( $id ); + $key = Key::find( $id ); if ( ! $key ) { continue; } diff --git a/includes/Models/Activation.php b/includes/Models/Activation.php index ef481ea4..94c9b6dd 100644 --- a/includes/Models/Activation.php +++ b/includes/Models/Activation.php @@ -148,7 +148,7 @@ public function get_key() { return null; } - return Key::get( $this->get_serial_id() ); + return Key::find( $this->get_serial_id() ); } /** @@ -304,43 +304,4 @@ public function save() { return parent::save(); } - - /* - |-------------------------------------------------------------------------- - | Query Methods - |-------------------------------------------------------------------------- - | - | Methods for reading and manipulating the object properties. - | - */ - - /** - * Translate legacy query arguments into the b8 query dialect. - * - * If order_id or product_id is given, the query is joined with the keys - * table and filtered by those columns. - * - * @param array $args Query arguments. - * @param \PluginEver\SerialNumbers\B8\Models\Query $query Query object. - * - * @since 1.0.0 - * @return array Translated query arguments. - */ - protected function prepare_query_args( $args, $query ) { - if ( ! empty( $args['order_id'] ) || ! empty( $args['product_id'] ) ) { - $key_table = ( new Key() )->get_table_name(); - $query->join( $key_table, "{$this->get_table()}.serial_id", "{$key_table}.id" ); - - if ( ! empty( $args['order_id'] ) ) { - $query->where( "{$key_table}.order_id", absint( $args['order_id'] ) ); - } - - if ( ! empty( $args['product_id'] ) ) { - $query->where( "{$key_table}.product_id", absint( $args['product_id'] ) ); - } - } - unset( $args['order_id'], $args['product_id'] ); - - return parent::prepare_query_args( $args, $query ); - } } diff --git a/includes/Models/Key.php b/includes/Models/Key.php index 26dde80e..c09a387a 100644 --- a/includes/Models/Key.php +++ b/includes/Models/Key.php @@ -590,7 +590,7 @@ public function save() { // Duplicate serial key is not allowed. if ( ! wcsn_is_duplicate_key_allowed() ) { - $existing = self::get( + $existing = static::find( array( 'serial_key' => $this->get_serial_key(), ) @@ -676,109 +676,77 @@ protected function perform_update( $id, $data ) { */ /** - * Retrieve the object instance. + * Bootstrap the model. * - * @param int|array|static $data Object ID or array of column conditions. + * Wires the encryption boundary into the native read and query hooks: + * the serial key is decrypted when read from the database and search + * terms are encrypted to match the value at rest. * * @since 1.0.0 - * - * @return static|false Object instance on success, false on failure. + * @return void */ - public static function get( $data ) { - // If by is set to serial key, encrypt it. - if ( is_array( $data ) && array_key_exists( 'serial_key', $data ) ) { - $data['serial_key'] = wcsn_encrypt_key( $data['serial_key'] ); - } - - return parent::get( $data ); + protected function bootstrap() { + parent::bootstrap(); + $this->on_filter( 'serial_key', 'wcsn_decrypt_key' ); + $this->on_filter( 'query_args', array( static::class, 'prepare_search' ), 10, 2 ); } /** - * Translate legacy query arguments into the b8 query dialect. + * Find a key by its primary key or column conditions. * - * Handles the customer_id constraint and the search clause, which must - * match the encrypted serial key at rest. + * The serial key is stored encrypted, so a serial_key condition is + * encrypted before it is matched against the database. * - * @param array $args Query arguments. - * @param \PluginEver\SerialNumbers\B8\Models\Query $query Query object. + * @param mixed $id The ID or array of column conditions. * * @since 1.0.0 - * @return array Translated query arguments. + * @return static|null The model instance, or null if not found. */ - protected function prepare_query_args( $args, $query ) { - // If customer id is set, find the orders having that customer id and limit the results to those orders. - if ( ! empty( $args['customer_id'] ) ) { - $order_ids = wc_get_orders( - array( - 'customer_id' => absint( $args['customer_id'] ), - 'limit' => - 1, - 'return' => 'ids', - ) - ); - - if ( ! empty( $order_ids ) ) { - $query->where( 'order_id', 'IN', $order_ids ); - } else { - $query->where( 'id', '=', 0 ); - } - } - unset( $args['customer_id'] ); - - // Search must compare the serial key column against the encrypted value. - if ( ! empty( $args['search'] ) ) { - $search = $args['search']; - if ( ! empty( $args['search_columns'] ) ) { - $search_columns = wp_parse_list( $args['search_columns'] ); - } else { - /** - * Filter the columns to search in when performing a search query. - * - * @param array $search_columns Array of columns to search in. - * @param array $args Query arguments. - * @param static $item Current instance of the class. - * - * @since 1.0.0 - */ - $search_columns = apply_filters( $this->get_hook_prefix() . '_search_columns', $this->get_searchable(), $args, $this ); - } - $search_columns = array_filter( array_unique( $search_columns ) ); - - if ( ! empty( $search_columns ) ) { - $query->where( - function ( $q ) use ( $search_columns, $search ) { - foreach ( $search_columns as $column ) { - $term = 'serial_key' === $column ? wcsn_encrypt_key( $search ) : $search; - $q->where( $column, 'LIKE', $term ); - } - }, - 'OR' - ); - } - - unset( $args['search'], $args['search_columns'] ); + public static function find( $id ) { + if ( is_array( $id ) && ! empty( $id['serial_key'] ) ) { + $id['serial_key'] = wcsn_encrypt_key( $id['serial_key'] ); } - return parent::prepare_query_args( $args, $query ); + return parent::find( $id ); } /** - * Prepare data read from the database before it is hydrated. + * Prepare the search query var. * - * The serial key is stored encrypted and decrypted on read. + * The serial key column holds encrypted values, so the search term is + * encrypted when compared against that column. * - * @param array $data Data read from the database. + * @param array $args Query arguments. + * @param \PluginEver\SerialNumbers\B8\Models\Query $query Query object. * * @since 1.0.0 - * @return array Prepared data. + * @return array Query arguments. */ - protected function prepare_db_data( $data ) { - $data = parent::prepare_db_data( $data ); + public static function prepare_search( $args, $query ) { + if ( empty( $args['search'] ) ) { + return $args; + } - if ( ! empty( $data['serial_key'] ) ) { - $data['serial_key'] = wcsn_decrypt_key( $data['serial_key'] ); + $search = $args['search']; + $columns = ! empty( $args['search_columns'] ) ? wp_parse_list( $args['search_columns'] ) : ( new static() )->get_searchable(); + $columns = array_filter( array_unique( (array) $columns ) ); + + if ( ! empty( $columns ) ) { + $query->where( + function ( $q ) use ( $columns, $search ) { + foreach ( $columns as $column ) { + $term = 'serial_key' === $column ? wcsn_encrypt_key( $search ) : $search; + $q->where( $column, 'LIKE', $term ); + } + }, + 'OR' + ); } - return $data; + $args['search'] = ''; + $args['search_columns'] = array(); + + return $args; } /* @@ -966,6 +934,6 @@ public function print_key( $masked = false ) { $key = sprintf( '%s', esc_attr( $key ), esc_attr( $key ), $key ); } - return apply_filters( $this->get_hook_prefix() . '_display_key', $key, $this ); + return $this->apply_filters( 'display_key', $key, $this ); } } diff --git a/includes/Models/Model.php b/includes/Models/Model.php index 85a15068..9371f4e2 100644 --- a/includes/Models/Model.php +++ b/includes/Models/Model.php @@ -7,9 +7,7 @@ /** * Class Model. * - * Base model built on the b8 models package. It also bridges the legacy - * framework-model API (getters/setters, query dialect and hooks) so existing - * call sites and extensions keep working unchanged. + * Base model built on the b8 models package. * * @since 1.0.0 * @package PluginEver\SerialNumbers\Models @@ -20,7 +18,7 @@ abstract class Model extends \PluginEver\SerialNumbers\B8\Models\Model { * Hook prefix for the model. * * Hooks are fired as "{hook_prefix}_{object_type}_{hook}", which resolves - * to the legacy "wc_serial_numbers_key_*" and "wc_serial_numbers_activation_*" names. + * to the "wc_serial_numbers_key_*" and "wc_serial_numbers_activation_*" names. * * @since 1.0.0 * @var string @@ -30,8 +28,6 @@ abstract class Model extends \PluginEver\SerialNumbers\B8\Models\Model { /** * Default query variables. * - * Matches the legacy query defaults (order by id ASC, 20 per page). - * * @since 1.0.0 * @var array */ @@ -41,352 +37,10 @@ abstract class Model extends \PluginEver\SerialNumbers\B8\Models\Model { 'limit' => 20, ); - /* - |-------------------------------------------------------------------------- - | Static Methods - |-------------------------------------------------------------------------- - | Legacy static entry points kept for backwards compatibility. - */ - - /** - * Retrieve the object instance. - * - * @param int|array|object|static $data Object ID, array of column conditions, or object. - * - * @since 1.0.0 - * @return static|false Object instance on success, false on failure. - */ - public static function get( $data ) { - if ( $data instanceof static ) { - $data = $data->get_key_value(); - } elseif ( is_object( $data ) ) { - $vars = get_object_vars( $data ); - $pk = ( new static() )->get_primary_key(); - $data = ! empty( $vars[ $pk ] ) ? $vars[ $pk ] : null; - } - - $item = static::find( $data ); - - return $item ? $item : false; - } - - /** - * Insert or update an object in the database. - * - * Accepts both the legacy ( $data, $wp_error ) and the b8 - * ( $data, $search, $wp_error ) call signatures. - * - * @param array|object $data Model data to insert or update. - * @param array|string|bool|null $search Search column(s), or the legacy $wp_error flag. - * @param bool $wp_error Whether to return a WP_Error on failure. - * - * @since 1.0.0 - * @return static|false|\WP_Error Object instance on success, false or WP_Error on failure. - */ - public static function insert( $data, $search = null, $wp_error = true ) { - // Legacy signature support: insert( $data, $wp_error ). - if ( is_bool( $search ) ) { - $wp_error = $search; - $search = null; - } - - return parent::insert( $data, $search, $wp_error ); - } - - /* - |-------------------------------------------------------------------------- - | CRUD Methods - |-------------------------------------------------------------------------- - */ - - /** - * Saves the object in the database. - * - * @since 1.0.0 - * @return static|\WP_Error Object instance on success, WP_Error on failure. - */ - public function save() { - /** - * Filters whether an item contains valid data before saving. - * - * Returning a WP_Error prevents the item from being saved. - * - * @param null|\WP_Error $check Null to allow saving, WP_Error to block. - * @param static $item Model object. - * - * @since 1.0.0 - */ - $check = apply_filters( $this->get_hook_prefix() . '_sanitize_data', null, $this ); - if ( is_wp_error( $check ) ) { - return $check; - } - - return parent::save(); - } - - /** - * Deletes the object from the database. - * - * @since 1.0.0 - * @return bool|mixed|\WP_Error True on success, false when the object does not exist. - */ - public function delete() { - if ( ! $this->exists() ) { - return false; - } - - /** - * Filters whether an item delete should take place. - * - * @param null|mixed $check Null to proceed with the deletion, any other value to short-circuit. - * @param static $item Model object. - * @param array $data Model data array. - * - * @since 1.0.0 - */ - $check = apply_filters( $this->get_hook_prefix() . '_check_delete', null, $this, $this->to_array() ); - if ( null !== $check ) { - return $check; - } - - return parent::delete(); - } - - /** - * Create an item in the database. - * - * @param array $data Data to be inserted. - * - * @since 1.0.0 - * @return \WP_Error|int The ID of the inserted item, or WP_Error on failure. - */ - protected function perform_create( $data ) { - /** - * Filters the data to be inserted into the database. - * - * @param array $data Data to be inserted. - * @param static $item Model object. - * - * @since 1.0.0 - */ - $data = apply_filters( $this->get_hook_prefix() . '_insert_data', $data, $this ); - - return parent::perform_create( $data ); - } - - /** - * Update an item in the database. - * - * @param int $id ID of the item to be updated. - * @param array $data Data to be updated. - * - * @since 1.0.0 - * @return \WP_Error|bool True on success, or WP_Error on failure. - */ - protected function perform_update( $id, $data ) { - /** - * Filters the data to be updated in the database. - * - * @param array $data Data to be updated. - * @param static $item Model object. - * - * @since 1.0.0 - */ - $data = apply_filters( $this->get_hook_prefix() . '_update_data', $data, $this ); - - return parent::perform_update( $id, $data ); - } - - /* - |-------------------------------------------------------------------------- - | Query Methods - |-------------------------------------------------------------------------- - */ - - /** - * Get the query object for the model, or run a query when args are given. - * - * @param array|null $args Optional. The query arguments. - * - * @since 1.0.0 - * @return \PluginEver\SerialNumbers\B8\Models\Query|array|int Query object, or the items/count when args are given. - */ - public function new_query( $args = null ) { - if ( is_null( $args ) ) { - return parent::new_query(); - } - - $args = (array) $args; - $output = isset( $args['output'] ) ? $args['output'] : null; - unset( $args['output'] ); - - $items = parent::new_query( $args ); - - // Legacy 'output' argument support. - if ( $output && is_array( $items ) && in_array( $output, array( ARRAY_A, ARRAY_N, OBJECT ), true ) ) { - foreach ( $items as $index => $item ) { - if ( ! $item instanceof self ) { - continue; - } - if ( ARRAY_A === $output ) { - $items[ $index ] = $item->to_array(); - } elseif ( ARRAY_N === $output ) { - $items[ $index ] = array_values( $item->to_array() ); - } else { - $items[ $index ] = (object) $item->to_array(); - } - } - } - - return $items; - } - - /** - * Apply filters with the automatic hook prefix. - * - * Intercepts the read and query extension points so legacy behavior - * (query-arg dialect, db data filters) applies on every query path. - * - * @param string $hook The filter name. - * @param mixed $value The value to filter. - * @param mixed ...$args Additional arguments. - * - * @since 1.0.0 - * @return mixed The filtered value. - */ - public function apply_filters( string $hook, $value, ...$args ) { - if ( 'query_args' === $hook && isset( $args[0] ) ) { - $value = $this->prepare_query_args( (array) $value, $args[0] ); - } elseif ( 'data' === $hook && is_array( $value ) ) { - $value = $this->prepare_db_data( $value ); - } - - return parent::apply_filters( $hook, $value, ...$args ); - } - - /** - * Translate legacy query arguments into the b8 query dialect. - * - * @param array $args Query arguments. - * @param \PluginEver\SerialNumbers\B8\Models\Query $query Query object. - * - * @since 1.0.0 - * @return array Translated query arguments. - */ - protected function prepare_query_args( $args, $query ) { - // 'paged' is the legacy name of 'page'. - if ( isset( $args['paged'] ) ) { - if ( empty( $args['page'] ) ) { - $args['page'] = max( 1, (int) $args['paged'] ); - } - unset( $args['paged'] ); - } - - // 'nopaging' and non-positive 'per_page'/'limit' disable the LIMIT clause. - if ( ! empty( $args['nopaging'] ) ) { - $args['limit'] = -1; - } - unset( $args['nopaging'] ); - if ( isset( $args['per_page'] ) && (int) $args['per_page'] < 1 ) { - $args['limit'] = -1; - unset( $args['per_page'] ); - } - if ( isset( $args['limit'] ) && (int) $args['limit'] < 1 ) { - $args['limit'] = -1; - } - - // 'fields' is the legacy name of 'select'. - if ( isset( $args['fields'] ) ) { - $fields = is_array( $args['fields'] ) ? $args['fields'] : preg_split( '/[,\s]+/', (string) $args['fields'] ); - if ( in_array( 'ids', (array) $fields, true ) ) { - $args['select'] = 'ids'; - } - unset( $args['fields'] ); - } - - // Found rows are now only calculated on demand. - unset( $args['no_count'] ); - - // Legacy multi-column 'orderby' values. - if ( ! empty( $args['orderby'] ) && is_string( $args['orderby'] ) && preg_match( '/[\s,]/', trim( $args['orderby'] ) ) ) { - $orderby = array(); - foreach ( array_filter( array_map( 'trim', explode( ',', $args['orderby'] ) ) ) as $part ) { - $part = preg_split( '/\s+/', $part ); - $orderby[] = array( $part[0] => isset( $part[1] ) ? $part[1] : '' ); - } - $args['orderby'] = array_merge( ...$orderby ); - } - if ( ! empty( $args['orderby'] ) && is_array( $args['orderby'] ) ) { - $order = isset( $args['order'] ) && is_string( $args['order'] ) ? strtoupper( $args['order'] ) : 'ASC'; - $orderby = array(); - foreach ( $args['orderby'] as $column => $direction ) { - if ( is_numeric( $column ) ) { - $column = $direction; - $direction = $order; - } - $direction = in_array( strtoupper( (string) $direction ), array( 'ASC', 'DESC' ), true ) ? strtoupper( $direction ) : $order; - $orderby[] = array( - 'column' => $column, - 'direction' => $direction, - ); - } - $args['orderby'] = $orderby; - } - - // Legacy 'where_query' clauses. - if ( ! empty( $args['where_query'] ) && is_array( $args['where_query'] ) ) { - foreach ( $args['where_query'] as $index => $clause ) { - if ( ! is_numeric( $index ) && is_string( $clause ) ) { - $clause = array( - 'column' => $index, - 'value' => $clause, - 'compare' => '=', - ); - } - $clause = wp_parse_args( - (array) $clause, - array( - 'column' => '', - 'value' => '', - 'compare' => '=', - ) - ); - if ( empty( $clause['column'] ) ) { - continue; - } - $query->where( $clause['column'], strtoupper( $clause['compare'] ), $clause['value'] ); - } - } - unset( $args['where_query'] ); - - return $args; - } - - /** - * Prepare data read from the database before it is hydrated. - * - * @param array $data Data read from the database. - * - * @since 1.0.0 - * @return array Prepared data. - */ - protected function prepare_db_data( $data ) { - /** - * Filters the data retrieved from the database. - * - * @param array $data Data retrieved from the database. - * @param static $item Model object. - * - * @since 1.0.0 - */ - return apply_filters( $this->get_hook_prefix() . '_db_data', $data, $this ); - } - /* |-------------------------------------------------------------------------- | Getters and Setters |-------------------------------------------------------------------------- - | Legacy prop accessors used by the concrete models and external code. */ /** @@ -398,13 +52,13 @@ protected function prepare_db_data( $data ) { * @since 1.0.0 * @return mixed The value of the attribute. */ - public function get_attribute( string $key, $fallback = null ) { + public function get( string $key, $fallback = null ) { $getter = "get_{$key}"; if ( ! method_exists( self::class, $getter ) && method_exists( $this, $getter ) ) { return $this->$getter(); } - return parent::get_attribute( $key, $fallback ); + return parent::get( $key, $fallback ); } /** @@ -436,34 +90,23 @@ public function set( string $key, $value ) { * @since 1.0.0 * @return mixed */ - protected function get_prop( $prop, $context = 'edit' ) { - $value = null; + protected function get_prop( $prop, $context = 'edit' ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Context kept for getter signatures. if ( array_key_exists( $prop, $this->attributes ) ) { - $value = $this->attributes[ $prop ]; - } elseif ( array_key_exists( $prop, $this->metadata ) ) { - $value = $this->metadata[ $prop ]; + return $this->attributes[ $prop ]; } - if ( 'view' === $context ) { - /** - * Filters the prop value when read for viewing. - * - * @param mixed $value Prop value. - * @param static $item Model object. - * - * @since 1.0.0 - */ - $value = apply_filters( $this->get_hook_prefix() . '_get_' . $prop, $value, $this ); + if ( array_key_exists( $prop, $this->metadata ) ) { + return $this->metadata[ $prop ]; } - return $value; + return null; } /** * Sets a prop for a setter method. * - * Only known columns and registered metadata are written, matching the - * legacy behavior of ignoring unknown props. + * Only known columns and registered metadata are written, unknown props + * are ignored. * * @param string $prop Name of prop to set. * @param mixed $value Value to set. @@ -479,28 +122,6 @@ protected function set_prop( $prop, $value ) { } } - /** - * Set the model's data. - * - * @param array|object $props Array or object of properties to set. - * - * @since 1.0.0 - * @return static - */ - public function set_data( $props ) { - return $this->fill( $props ); - } - - /** - * Get the model's data. - * - * @since 1.0.0 - * @return array - */ - public function get_data() { - return $this->to_array(); - } - /** * Get date prop. * @@ -512,23 +133,9 @@ public function get_data() { * @return string|null */ public function get_date_prop( $prop, $context = 'edit', $format = 'Y-m-d H:i:s' ) { - $datetime = $this->sanitize_date( $this->get_prop( $prop ) ); - - $value = $datetime ? date( $format, strtotime( $datetime ) ) : null; // @codingStandardsIgnoreLine - date() is ok here. - - if ( 'view' === $context ) { - /** - * Filters the date prop value when read for viewing. - * - * @param mixed $value Prop value. - * @param static $item Model object. - * - * @since 1.0.0 - */ - $value = apply_filters( $this->get_hook_prefix() . '_get_' . $prop, $value, $this ); - } + $datetime = $this->sanitize_date( $this->get_prop( $prop, $context ) ); - return $value; + return $datetime ? date( $format, strtotime( $datetime ) ) : null; // @codingStandardsIgnoreLine - date() is ok here. } /** @@ -555,29 +162,6 @@ public function set_date_prop( $prop, $value, $format = 'Y-m-d H:i:s' ) { |-------------------------------------------------------------------------- */ - /** - * Get the hook prefix including the object type. - * - * Kept for backwards compatibility with the legacy model API, where the - * prefix included the object type (e.g. "wc_serial_numbers_key"). - * - * @since 1.0.0 - * @return string - */ - public function get_hook_prefix(): string { - return $this->hook_prefix . '_' . $this->object_type; - } - - /** - * Get table name. - * - * @since 1.0.0 - * @return string - */ - public function get_table_name() { - return $this->get_table(); - } - /** * Checks if a date is valid or not. * diff --git a/includes/RestAPI/Controllers/Software.php b/includes/RestAPI/Controllers/Software.php index 477f5179..6480000e 100644 --- a/includes/RestAPI/Controllers/Software.php +++ b/includes/RestAPI/Controllers/Software.php @@ -144,7 +144,7 @@ public function validate_request( $request ) { } // Check if key exists. - $serial_key = Key::get( + $serial_key = Key::find( array( 'serial_key' => $key, 'product_id' => $product_id, @@ -199,7 +199,7 @@ public function validate_key( $request ) { $product_id = absint( $request->get_param( 'product_id' ) ); $key = sanitize_text_field( $request->get_param( 'serial_key' ) ); - $serial_key = Key::get( + $serial_key = Key::find( array( 'serial_key' => $key, 'product_id' => $product_id, @@ -216,11 +216,11 @@ public function validate_key( $request ) { '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, - ) + 'activations' => array_map( + function ( $activation ) { + return $activation->to_array(); + }, + $serial_key->get_activations( array( 'limit' => - 1 ) ) ), // Deprecated. @@ -250,7 +250,7 @@ public function activate_key( $request ) { $instance = md5( $email . $platform . time() ); } - $serial_key = Key::get( + $serial_key = Key::find( array( 'serial_key' => $key, 'product_id' => $product_id, @@ -258,7 +258,7 @@ public function activate_key( $request ) { ); // Check if instance is already activated. - $activation = Activation::get( + $activation = Activation::find( array( 'serial_id' => $serial_key->get_id(), 'instance' => $instance, @@ -297,11 +297,11 @@ public function activate_key( $request ) { '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, - ) + 'activations' => array_map( + function ( $activation ) { + return $activation->to_array(); + }, + $serial_key->get_activations( array( 'limit' => - 1 ) ) ), // Deprecated. @@ -328,14 +328,14 @@ public function deactivate_key( $request ) { return new \WP_Error( 'missing_instance', __( 'Instance is missing, You must provide an instance to deactivate license.', 'wc-serial-numbers' ), array( 'status' => 400 ) ); } - $serial_key = Key::get( + $serial_key = Key::find( array( 'serial_key' => $key, 'product_id' => $product_id, ) ); - $activation = Activation::get( + $activation = Activation::find( array( 'serial_id' => $serial_key->get_id(), 'instance' => $instance, @@ -360,11 +360,11 @@ public function deactivate_key( $request ) { '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, - ) + 'activations' => array_map( + function ( $activation ) { + return $activation->to_array(); + }, + $serial_key->get_activations( array( 'limit' => - 1 ) ) ), // Deprecated. @@ -386,7 +386,7 @@ public function version_check( $request ) { $product_id = absint( $request->get_param( 'product_id' ) ); $key = sanitize_text_field( $request->get_param( 'serial_key' ) ); - $serial_key = Key::get( + $serial_key = Key::find( array( 'serial_key' => $key, 'product_id' => $product_id, diff --git a/includes/functions.php b/includes/functions.php index 7034c21e..cd4463e4 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -162,7 +162,7 @@ function wcsn_get_order_object( $order ) { * @return Key|WP_Error object on success, WP_Error object on failure. */ function wcsn_insert_key( $args, $wp_error = true ) { - return Key::insert( $args, $wp_error ); + return Key::insert( $args, null, $wp_error ); } /** @@ -180,9 +180,32 @@ function wcsn_get_keys( $args = array(), $count = false ) { 'offset' => 0, 'orderby' => 'id', 'order' => 'DESC', - 'fields' => 'all', ); $args = wp_parse_args( $args, $defaults ); + + // Map the function's public per_page/paged args to the native limit/page args. + if ( isset( $args['per_page'] ) ) { + $args['limit'] = (int) $args['per_page']; + unset( $args['per_page'] ); + } + if ( isset( $args['paged'] ) ) { + $args['page'] = max( 1, (int) $args['paged'] ); + unset( $args['paged'] ); + } + + // Constrain to the orders of the given customer. + if ( ! empty( $args['customer_id'] ) ) { + $order_ids = wc_get_orders( + array( + 'customer_id' => absint( $args['customer_id'] ), + 'limit' => - 1, + 'return' => 'ids', + ) + ); + $args['order_id__in'] = ! empty( $order_ids ) ? $order_ids : array( 0 ); + } + unset( $args['customer_id'] ); + if ( $count ) { return Key::count( $args ); } @@ -199,7 +222,9 @@ function wcsn_get_keys( $args = array(), $count = false ) { * @return Key|false */ function wcsn_get_key( $key ) { - return Key::get( $key ); + $key = Key::find( $key ); + + return $key ? $key : false; } /** @@ -246,14 +271,45 @@ function wcsn_get_activations( $args = array(), $count = false ) { 'offset' => 0, 'orderby' => 'id', 'order' => 'DESC', - 'fields' => 'all', ); $args = wp_parse_args( $args, $defaults ); + + // Map the function's public per_page/paged args to the native limit/page args. + if ( isset( $args['per_page'] ) ) { + $args['limit'] = (int) $args['per_page']; + unset( $args['per_page'] ); + } + if ( isset( $args['paged'] ) ) { + $args['page'] = max( 1, (int) $args['paged'] ); + unset( $args['paged'] ); + } + + // order_id and product_id are key columns, constrain through the keys table. + $order_id = ! empty( $args['order_id'] ) ? absint( $args['order_id'] ) : 0; + $product_id = ! empty( $args['product_id'] ) ? absint( $args['product_id'] ) : 0; + unset( $args['order_id'], $args['product_id'] ); + + $query = Activation::query()->set_var( $args ); + + if ( $order_id || $product_id ) { + $key_table = ( new Key() )->get_table(); + $activation_table = ( new Activation() )->get_table(); + $query->join( $key_table, "{$activation_table}.serial_id", "{$key_table}.id" ); + + if ( $order_id ) { + $query->where( "{$key_table}.order_id", $order_id ); + } + + if ( $product_id ) { + $query->where( "{$key_table}.product_id", $product_id ); + } + } + if ( $count ) { - return Activation::count( $args ); + return $query->count(); } - return Activation::query( $args ); + return $query->get(); } /** @@ -265,7 +321,9 @@ function wcsn_get_activations( $args = array(), $count = false ) { * @return Activation|false */ function wcsn_get_activation( $activation ) { - return Activation::get( $activation ); + $activation = Activation::find( $activation ); + + return $activation ? $activation : false; } /** @@ -447,7 +505,6 @@ function wcsn_order_get_unfulfilled_items( $order_id ) { */ function wcsn_order_update_keys( $order_id ) { $order = wcsn_get_order_object( $order_id ); - $customer_id = $order->get_customer_id(); $line_items = wcsn_get_order_line_items_data( $order ); $revoke_statues = wcsn_get_revoke_statuses(); $order_status = $order->get_status( 'edit' ); @@ -549,12 +606,11 @@ function wcsn_order_update_keys( $order_id ) { // Assign keys to order. foreach ( $keys as $key ) { - $key->set_data( + $key->fill( array( 'order_id' => $order_id, 'order_item_id' => $item['order_item_id'], 'order_date' => $order->get_date_created() ? $order->get_date_created()->format( 'Y-m-d H:i:s' ) : current_time( 'mysql' ), - 'customer_id' => $customer_id, 'status' => 'sold', ) ); @@ -679,7 +735,7 @@ function wcsn_order_remove_keys( $order_id, $product_id = null ) { $props['order_item_id'] = 0; $props['order_date'] = null; } - $key->set_data( $props ); + $key->fill( $props ); $key->save(); } @@ -713,7 +769,6 @@ function wcsn_order_replace_key( $order_id, $product_id = null, $key_id = null ) $args = array( 'order_id' => $order_id, 'limit' => - 1, - 'no_count' => true, ); if ( ! empty( $product_id ) ) { @@ -752,7 +807,7 @@ function wcsn_order_replace_key( $order_id, $product_id = null, $key_id = null ) $props['order_item_id'] = 0; $props['order_date'] = null; } - $key->set_data( $props ); + $key->fill( $props ); if ( $key->save() ) { ++$replaced; } diff --git a/tests/phpunit/KeyModelTest.php b/tests/phpunit/KeyModelTest.php index 3a287809..f598cd9b 100644 --- a/tests/phpunit/KeyModelTest.php +++ b/tests/phpunit/KeyModelTest.php @@ -27,7 +27,7 @@ public function testInsertPersistsAndRoundtrips(): void { $this->assertNotEmpty( $key->get_id() ); // Re-read from the database and confirm the plain key round-trips. - $reloaded = Key::get( $key->get_id() ); + $reloaded = Key::find( $key->get_id() ); $this->assertSame( 'SERIAL-123-ABC', $reloaded->get_serial_key() ); $this->assertSame( 'SERIAL-123-ABC', $reloaded->get_key() ); $this->assertSame( $product->get_id(), $reloaded->get_product_id() ); @@ -112,7 +112,7 @@ public function testStatusTransitionAvailableToSold(): void { $key->set_status( 'sold' ); $this->assertNotWPError( $key->save() ); - $reloaded = Key::get( $key->get_id() ); + $reloaded = Key::find( $key->get_id() ); $this->assertSame( 'sold', $reloaded->get_status() ); } @@ -133,7 +133,7 @@ public function testDeleteRemovesKey(): void { $id = $key->get_id(); $this->assertTrue( (bool) wcsn_delete_key( $id ) ); - $this->assertEmpty( Key::get( $id ) ); + $this->assertEmpty( Key::find( $id ) ); $this->assertFalse( wcsn_get_key( $id ) ); } } diff --git a/tests/phpunit/OrderDeliveryTest.php b/tests/phpunit/OrderDeliveryTest.php index a3f3c1bf..f60a47d8 100644 --- a/tests/phpunit/OrderDeliveryTest.php +++ b/tests/phpunit/OrderDeliveryTest.php @@ -40,7 +40,7 @@ public function testCompletedOrderAssignsKey(): void { $order = $this->create_order( $product ); $order->update_status( 'completed' ); - $reloaded = Key::get( $key->get_id() ); + $reloaded = Key::find( $key->get_id() ); $this->assertSame( $order->get_id(), $reloaded->get_order_id() ); $this->assertSame( 'sold', $reloaded->get_status() ); $this->assertNotEmpty( $reloaded->get_order_item_id() ); @@ -67,7 +67,7 @@ public function testProcessingOrderAssignsKey(): void { $order = $this->create_order( $product ); $order->update_status( 'processing' ); - $reloaded = Key::get( $key->get_id() ); + $reloaded = Key::find( $key->get_id() ); $this->assertSame( $order->get_id(), $reloaded->get_order_id() ); $this->assertSame( 'sold', $reloaded->get_status() ); } @@ -88,7 +88,7 @@ public function testCancelledOrderRevokesKey(): void { $order->update_status( 'completed' ); $order->update_status( 'cancelled' ); - $reloaded = Key::get( $key->get_id() ); + $reloaded = Key::find( $key->get_id() ); $this->assertSame( 'cancelled', $reloaded->get_status() ); $this->assertSame( 0, $reloaded->get_order_id() ); }