check_requirements(); $this->includes(); $this->init_hooks(); } /** * Check plugin requirements */ private function check_requirements() { // Check if WooCommerce is active // `active_plugins` is a WordPress core filter being read, not a hook // this plugin defines, so the plugin-prefix rule does not apply. // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- reading a core filter if (!in_array('woocommerce/woocommerce.php', (array) apply_filters('active_plugins', get_option('active_plugins')), true)) { add_action('admin_notices', [$this, 'woocommerce_missing_notice']); return; } } /** * WooCommerce missing notice */ public function woocommerce_missing_notice() { ?>

post_type !== 'product' && $post->post_type !== 'product_variation') { return; } if ($this->product_mapper) { $this->product_mapper->unmap($post_id); } } /** * Initialize plugin components */ public function init_components() { // Check WooCommerce again if (!class_exists('WooCommerce')) { return; } // Initialize components $this->logger = new Logger(); $this->api_client = new API_Client($this->logger); $this->price_sync = new Price_Sync($this->api_client, $this->logger); $this->stock_sync = new Stock_Sync($this->api_client, $this->logger); $this->cron = new Cron($this->price_sync, $this->stock_sync, $this->logger); $this->product_mapper = new Product_Mapper($this->logger); $this->product_importer = new Product_Importer($this->api_client, $this->price_sync, $this->stock_sync, $this->product_mapper, $this->logger); $this->order_handler = new Order_Handler($this->api_client, $this->product_mapper, $this->logger); $this->auto_importer = new Auto_Importer($this->product_importer, $this->api_client, $this->logger); // Ensure mapping table exists (for upgrades from older versions) // Move any data still stored under the old `dsz_` prefix before // anything tries to read it $this->maybe_migrate_prefix(); $this->maybe_create_mapping_table(); // Register auto import cron hook add_action('dszsync_auto_import_cron_hook', [$this, 'run_auto_import']); // Daily maintenance (log retention, orphaned mappings, stale transients) add_action('dszsync_maintenance_hook', [$this, 'run_maintenance']); if (!wp_next_scheduled('dszsync_maintenance_hook')) { wp_schedule_event(time() + DAY_IN_SECONDS, 'daily', 'dszsync_maintenance_hook'); } // Tracking sync: poll DSZ twice daily for tracking/status on submitted orders add_action('dszsync_tracking_sync_hook', [$this, 'run_tracking_sync']); if (!wp_next_scheduled('dszsync_tracking_sync_hook')) { wp_schedule_event(time() + HOUR_IN_SECONDS, 'twicedaily', 'dszsync_tracking_sync_hook'); } // Auto-submit paid orders to DSZ when enabled (middleman flow) add_action('woocommerce_order_status_processing', [$this, 'maybe_auto_submit_order'], 20); if (is_admin()) { $this->admin_ui = new Admin_UI($this->api_client, $this->price_sync, $this->stock_sync, $this->cron, $this->logger, $this->product_mapper, $this->product_importer, $this->order_handler, $this->auto_importer); } // Register shipping method (after WooCommerce shipping is initialized) add_action('woocommerce_shipping_init', [$this, 'load_shipping_method']); add_filter('woocommerce_shipping_methods', [$this, 'register_shipping_method']); // One-time zone attachment after activation (deferred until WC is ready) add_action('admin_init', [$this, 'maybe_setup_shipping_zones']); } /** * Load the shipping method class after WooCommerce is ready */ public function load_shipping_method() { require_once DSZSYNC_PLUGIN_DIR . 'includes/class-shipping-method.php'; } /** * Register DSZ shipping method with WooCommerce * * @param array $methods Existing shipping methods. * @return array Modified shipping methods. */ public function register_shipping_method($methods) { $methods['dszsync_shipping'] = 'Dropshipzone\\Shipping_Method'; return $methods; } /** * Migrate data stored under the old `dsz_` prefix to `dszsync_`. * * The prefix was widened to satisfy the plugin directory's four-character * minimum. Options, post meta, cron hooks and the shipping method id all * moved; database table and column names did not, because they are scoped * inside plugin-owned tables and cannot collide with anything. * * Runs once, gated by an option flag. */ private function maybe_migrate_prefix() { if (get_option('dszsync_prefix_migrated')) { return; } global $wpdb; // Options: copy old value across, then drop the old row $option_map = [ 'dsz_sync_settings' => 'dszsync_settings', 'dsz_sync_price_rules' => 'dszsync_price_rules', 'dsz_sync_stock_rules' => 'dszsync_stock_rules', 'dsz_sync_import_settings' => 'dszsync_import_settings', 'dsz_sync_api_email' => 'dszsync_api_email', 'dsz_sync_api_password' => 'dszsync_api_password', 'dsz_sync_api_token' => 'dszsync_api_token', 'dsz_sync_token_expiry' => 'dszsync_token_expiry', 'dsz_auto_import_settings' => 'dszsync_auto_import_settings', 'dsz_auto_import_state' => 'dszsync_auto_import_state', 'dsz_auto_import_history' => 'dszsync_auto_import_history', 'dsz_rate_limit_data' => 'dszsync_rate_limit_data', 'dsz_order_settings' => 'dszsync_order_settings', 'dsz_import_templates' => 'dszsync_import_templates', 'dsz_price_rules_v2' => 'dszsync_price_rules_v2', 'dsz_log_retention_days' => 'dszsync_log_retention_days', 'dsz_mapping_schema_version' => 'dszsync_mapping_schema_version', 'dsz_setup_shipping_pending' => 'dszsync_setup_shipping_pending', ]; foreach ($option_map as $old => $new) { $value = get_option($old, null); if ($value !== null && get_option($new, null) === null) { update_option($new, $value, false); } delete_option($old); } // Post meta $meta_map = [ '_dsz_cost' => '_dszsync_cost', '_dsz_serial_number' => '_dszsync_serial_number', '_dsz_submitted_at' => '_dszsync_submitted_at', '_dsz_not_available' => '_dszsync_not_available', '_dsz_imported_image' => '_dszsync_imported_image', '_dsz_tracking_number' => '_dszsync_tracking_number', '_dsz_courier' => '_dszsync_courier', ]; foreach ($meta_map as $old => $new) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies. $wpdb->update($wpdb->postmeta, ['meta_key' => $new], ['meta_key' => $old], ['%s'], ['%s']); } // Order meta lives in its own table under HPOS $hpos_meta = $wpdb->prefix . 'wc_orders_meta'; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies. if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $hpos_meta)) === $hpos_meta) { foreach ($meta_map as $old => $new) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies. $wpdb->update($hpos_meta, ['meta_key' => $new], ['meta_key' => $old], ['%s'], ['%s']); } } // Shipping method id, so configured zones keep working $zone_methods = $wpdb->prefix . 'woocommerce_shipping_zone_methods'; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies. if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $zone_methods)) === $zone_methods) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies. $wpdb->update($zone_methods, ['method_id' => 'dszsync_shipping'], ['method_id' => 'dsz_shipping'], ['%s'], ['%s']); } // Per-instance shipping settings are stored as options named // woocommerce___settings // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies. $shipping_options = $wpdb->get_col($wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like('woocommerce_dsz_shipping_') . '%' )); foreach ((array) $shipping_options as $old_name) { $new_name = str_replace('woocommerce_dsz_shipping_', 'woocommerce_dszsync_shipping_', $old_name); $value = get_option($old_name, null); if ($value !== null && get_option($new_name, null) === null) { update_option($new_name, $value, false); } delete_option($old_name); } // Old cron hooks: clear them; the new ones are scheduled on init foreach ([ 'dsz_sync_cron_hook', 'dsz_auto_import_cron_hook', 'dsz_maintenance_hook', 'dsz_tracking_sync_hook', 'dsz_sync_batch_continue', ] as $old_hook) { wp_clear_scheduled_hook($old_hook); } update_option('dszsync_prefix_migrated', 1, false); } /** * Create mapping table if it doesn't exist and run migrations */ private function maybe_create_mapping_table() { // Schema version gate — avoids an information_schema query on every load. // Bump whenever the table structure changes. // v3: composite index (sync_enabled, last_synced) $schema_version = '3'; if (get_option('dszsync_mapping_schema_version') === $schema_version) { return; } global $wpdb; $table_name = $wpdb->prefix . 'dsz_product_mapping'; // Check if table exists // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies. $table_exists = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(1) FROM information_schema.tables WHERE table_schema = %s AND table_name = %s", DB_NAME, $table_name )); if (!$table_exists) { Product_Mapper::create_table(); } else { // Run migrations for existing installations Product_Mapper::maybe_add_last_resynced_column(); Product_Mapper::maybe_add_indexes(); } update_option('dszsync_mapping_schema_version', $schema_version, false); } /** * Plugin activation */ public function activate() { // Create log table $this->create_log_table(); // Create mapping table Product_Mapper::create_table(); // Create orders table Order_Handler::create_table(); // Create sync status option $this->create_default_options(); // Schedule cron if (!wp_next_scheduled('dszsync_cron_hook')) { wp_schedule_event(time(), 'hourly', 'dszsync_cron_hook'); } // Defer shipping zone setup to the next admin load — WooCommerce's // shipping registry is not reliably available during activation update_option('dszsync_setup_shipping_pending', 1, false); // Flush rewrite rules flush_rewrite_rules(); } /** * One-time setup: attach the DSZ shipping method to AU/NZ zones. * * Dropshipzone ships the products directly, so the store must quote * their shipping rates — without a zone assignment the method never * appears at checkout. Idempotent: skipped when any zone already has * the method (merchants can still remove/re-arrange it manually). */ public function maybe_setup_shipping_zones() { if (!get_option('dszsync_setup_shipping_pending')) { return; } if (!class_exists('WC_Shipping_Zones') || !class_exists('WC_Shipping_Zone')) { return; // WooCommerce not ready — retry next admin load } delete_option('dszsync_setup_shipping_pending'); $zones = \WC_Shipping_Zones::get_zones(); // Already attached somewhere? Respect the merchant's setup. foreach ($zones as $zone_data) { foreach ($zone_data['shipping_methods'] as $method) { if ($method->id === 'dszsync_shipping') { return; } } } $targets = [ 'AU' => __('Australia', '3s-soft-price-stock-sync-for-dropshipzone'), 'NZ' => __('New Zealand', '3s-soft-price-stock-sync-for-dropshipzone'), ]; foreach ($targets as $country => $label) { $target_zone = null; // Reuse an existing zone that covers this country foreach ($zones as $zone_data) { foreach ($zone_data['zone_locations'] as $location) { if ($location->type === 'country' && $location->code === $country) { $target_zone = new \WC_Shipping_Zone($zone_data['id']); break; } } if ($target_zone) { break; } } if (!$target_zone) { $target_zone = new \WC_Shipping_Zone(); $target_zone->set_zone_name($label); $target_zone->add_location($country, 'country'); $target_zone->save(); } $target_zone->add_shipping_method('dszsync_shipping'); if ($this->logger) { $this->logger->info('DSZ shipping method attached to zone', [ 'zone' => $target_zone->get_zone_name(), 'country' => $country, ]); } } } /** * Plugin deactivation */ public function deactivate() { // Clear scheduled cron wp_clear_scheduled_hook('dszsync_cron_hook'); wp_clear_scheduled_hook('dszsync_auto_import_cron_hook'); wp_clear_scheduled_hook('dszsync_maintenance_hook'); wp_clear_scheduled_hook('dszsync_tracking_sync_hook'); wp_clear_scheduled_hook('dszsync_batch_continue'); wp_clear_scheduled_hook('dszsync_incremental_hook'); // Flush rewrite rules flush_rewrite_rules(); } /** * Run auto import (cron callback) */ public function run_auto_import() { if ($this->auto_importer) { $this->auto_importer->run_import(); } } /** * Tracking sync (cron callback) */ public function run_tracking_sync() { if ($this->order_handler) { $this->order_handler->sync_tracking(); } } /** * Auto-submit a paid order to Dropshipzone when the option is enabled * * @param int $order_id WooCommerce order ID */ public function maybe_auto_submit_order($order_id) { $settings = get_option('dszsync_order_settings', []); if (empty($settings['auto_submit']) || !$this->order_handler) { return; } if (!$this->order_handler->order_has_dszsync_products($order_id)) { return; } $result = $this->order_handler->submit_order($order_id); if (is_wp_error($result) && $result->get_error_code() !== 'already_submitted' && $this->logger) { $this->logger->error('Auto-submit failed', [ 'order_id' => $order_id, 'error' => $result->get_error_message(), ]); } } /** * Daily maintenance (cron callback) */ public function run_maintenance() { global $wpdb; // Log retention (days, filterable) $retention = apply_filters('dszsync_log_retention_days', intval(get_option('dszsync_log_retention_days', 30))); if ($this->logger) { $this->logger->cleanup_older_than($retention); } // Mappings pointing at deleted products if ($this->product_mapper) { $this->product_mapper->cleanup_orphaned(); } // Expired DSZ transients (zone/rate caches) that WP never got to reap // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is built from $wpdb->prefix and is not user input; all values are passed through prepare(). These are plugin-owned tables, so no core caching API applies. $wpdb->query($wpdb->prepare( "DELETE a, b FROM {$wpdb->options} a INNER JOIN {$wpdb->options} b ON b.option_name = CONCAT('_transient_', SUBSTRING(a.option_name, 20)) WHERE a.option_name LIKE %s AND a.option_value < %d", $wpdb->esc_like('_transient_timeout_dszsync_') . '%', time() )); if ($this->logger) { $this->logger->debug('Maintenance run completed', ['log_retention_days' => $retention]); } } /** * Create log table */ private function create_log_table() { global $wpdb; $table_name = $wpdb->prefix . 'dsz_sync_logs'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, level varchar(20) NOT NULL DEFAULT 'info', message text NOT NULL, context longtext, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY level (level), KEY created_at (created_at) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); } /** * Create default options */ private function create_default_options() { // API Settings if (!get_option('dszsync_api_email')) { add_option('dszsync_api_email', ''); } if (!get_option('dszsync_api_password')) { add_option('dszsync_api_password', ''); } if (!get_option('dszsync_api_token')) { add_option('dszsync_api_token', ''); } if (!get_option('dszsync_token_expiry')) { add_option('dszsync_token_expiry', 0); } // Price Rules $price_defaults = [ 'markup_type' => 'percentage', // percentage or fixed 'markup_value' => 30, 'rounding_enabled' => true, 'rounding_type' => '99', // 99, 95, nearest 'gst_enabled' => true, 'gst_type' => 'include', // include or exclude ]; if (!get_option('dszsync_price_rules')) { add_option('dszsync_price_rules', $price_defaults); } // Stock Rules $stock_defaults = [ 'buffer_enabled' => false, 'buffer_amount' => 0, 'zero_on_unavailable' => true, 'auto_out_of_stock' => true, ]; if (!get_option('dszsync_stock_rules')) { add_option('dszsync_stock_rules', $stock_defaults); } // Sync Settings $sync_defaults = [ 'frequency' => 'hourly', // hourly, twicedaily, daily 'batch_size' => 100, 'last_sync' => null, 'sync_in_progress' => false, 'current_offset' => 0, 'products_updated' => 0, 'errors_count' => 0, ]; if (!get_option('dszsync_settings')) { add_option('dszsync_settings', $sync_defaults); } } /** * Load plugin text domain */ public function load_textdomain() { // load_plugin_textdomain('3s-soft-price-stock-sync-for-dropshipzone', false, dirname(DSZSYNC_PLUGIN_BASENAME) . '/languages'); } /** * Declare HPOS compatibility */ public function declare_hpos_compatibility() { if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) { \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', DSZSYNC_PLUGIN_FILE, true); } } } /** * Initialize the plugin */ function dszsync_sync() { return Dropshipzone_Sync::instance(); } // Start the plugin dszsync_sync();