hp
/*
Plugin Name: Product Machine
Description: A plugin to process product data and prepare it for WooCommerce import.
Version: 1.3
Author: bucknelius
wp-content/plugins/product-machine/product-machine.php
*/
// from_hostgator_for_5th/public_html/wp-content/plugins/product-machine/product-machine.php
// Exit if accessed directly
if (!defined('ABSPATH')) {
exit;
}
// Global variable to collect errors
global $pm_errors;
$pm_errors = [];
// // Custom error handler function pm_error_handler($errno, $errstr, $errfile, $errline) {
// require_once plugin_dir_path(__FILE__) . 'includes/error-handling.php';
// set_error_handler('pm_error_handler');
// Add main menu and submenu pages function product_machine_menu() {
require_once plugin_dir_path(__FILE__) . 'includes/admin-menu.php';
// Include the Preview Data Handler function pm_preview_data()
require_once plugin_dir_path(__FILE__) . 'includes/preview-data-handler.php';
// Function to read the last N lines of a file function pm_get_last_lines_of_file($filepath, $lines = 100)
require_once plugin_dir_path(__FILE__) . 'includes/utility-functions.php';
// function product_machine_export_csv($products)
require_once plugin_dir_path(__FILE__) . 'includes/export-csv.php';
//function product_machine_process_raw_data($raw_data, $invoice_base, $sku_usage)
require_once plugin_dir_path(__FILE__) . 'includes/data-processing.php';
//function pm_get_external_db_connection()
require_once plugin_dir_path(__FILE__) . 'includes/database-connection.php';
// Include the Admin Scripts Handler function product_machine_admin_scripts($hook)
require_once plugin_dir_path(__FILE__) . 'includes/admin-scripts-handler.php';
// Admin helper: generate SKU button
require_once plugin_dir_path(__FILE__) . 'includes/admin-generate-sku.php';
// Create truck sizes table on plugin activation
function product_machine_create_truck_sizes_table() {
$conn = pm_get_external_db_connection();
if (!$conn) {
error_log('Product Machine: Could not connect to external database, using WordPress database for truck sizes table');
product_machine_create_truck_sizes_table_wp();
return;
}
$sql = "CREATE TABLE IF NOT EXISTS truck_sizes (
id INT AUTO_INCREMENT PRIMARY KEY,
brand VARCHAR(100) NOT NULL,
model VARCHAR(100) NOT NULL,
hanger_width_mm DECIMAL(5,2) NOT NULL,
axle_width_inches DECIMAL(6,3) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_truck (brand, model)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
if ($conn->query($sql) === TRUE) {
error_log('Product Machine: truck_sizes table created successfully');
product_machine_populate_sample_truck_sizes();
} else {
error_log('Product Machine: Error creating truck_sizes table: ' . $conn->error);
}
$conn->close();
}
// Create truck sizes table in WordPress database as fallback
function product_machine_create_truck_sizes_table_wp() {
global $wpdb;
$table_name = $wpdb->prefix . 'product_machine_truck_sizes';
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
id INT AUTO_INCREMENT PRIMARY KEY,
brand VARCHAR(100) NOT NULL,
model VARCHAR(100) NOT NULL,
hanger_width_mm DECIMAL(5,2) NOT NULL,
axle_width_inches DECIMAL(6,3) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_truck (brand, model)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
error_log('Product Machine: truck_sizes table created in WordPress database');
product_machine_populate_sample_truck_sizes_wp();
}
// Populate sample truck sizes
function product_machine_populate_sample_truck_sizes() {
$conn = pm_get_external_db_connection();
if (!$conn) {
return;
}
// Common skateboard truck sizes with accurate axle widths
// Based on real truck specifications, axle widths are typically in 1/8" increments
$truck_sizes = [
['ACE', '44', 44.00, 8.000],
['ACE', '55', 55.00, 8.250],
['ACE', '66', 66.00, 8.500],
['Independent', '139', 139.00, 7.875],
['Independent', '144', 144.00, 8.125],
['Independent', '149', 149.00, 8.375],
['Independent', '159', 159.00, 8.625],
['Independent', '169', 169.00, 8.875],
['Thunder', '143', 143.00, 8.000],
['Thunder', '147', 147.00, 8.250],
['Thunder', '151', 151.00, 8.500],
['Thunder', '161', 161.00, 8.750],
['Venture', '5.0', 127.00, 7.750],
['Venture', '5.25', 133.00, 8.000],
['Venture', '5.8', 147.00, 8.250],
['Venture', '6.1', 155.00, 8.500],
['Krux', '8.0', 203.00, 8.000],
['Krux', '8.25', 210.00, 8.250],
['Krux', '8.5', 216.00, 8.500],
['Tensor', '5.25', 133.00, 8.000],
['Tensor', '5.5', 140.00, 8.125],
['Paris', '150mm', 150.00, 8.250],
['Paris', '180mm', 180.00, 8.750],
['Bear', '852', 216.00, 8.500],
['Bear', '840', 203.00, 8.000],
['Caliber', '44°', 184.00, 8.500],
['Caliber', '50°', 184.00, 8.500]
];
$stmt = $conn->prepare("INSERT IGNORE INTO truck_sizes (brand, model, hanger_width_mm, axle_width_inches) VALUES (?, ?, ?, ?)");
foreach ($truck_sizes as $truck) {
$stmt->bind_param("ssdd", $truck[0], $truck[1], $truck[2], $truck[3]);
$stmt->execute();
}
$stmt->close();
$conn->close();
error_log('Product Machine: Sample truck sizes populated');
}
// Populate sample truck sizes in WordPress database
function product_machine_populate_sample_truck_sizes_wp() {
global $wpdb;
$table_name = $wpdb->prefix . 'product_machine_truck_sizes';
// Common skateboard truck sizes with accurate axle widths
$truck_sizes = [
['ACE', '44', 44.00, 8.000],
['ACE', '55', 55.00, 8.250],
['ACE', '66', 66.00, 8.500],
['Independent', '139', 139.00, 7.875],
['Independent', '144', 144.00, 8.125],
['Independent', '149', 149.00, 8.375],
['Independent', '159', 159.00, 8.625],
['Independent', '169', 169.00, 8.875],
['Thunder', '143', 143.00, 8.000],
['Thunder', '147', 147.00, 8.250],
['Thunder', '151', 151.00, 8.500],
['Thunder', '161', 161.00, 8.750],
['Venture', '5.0', 127.00, 7.750],
['Venture', '5.25', 133.00, 8.000],
['Venture', '5.8', 147.00, 8.250],
['Venture', '6.1', 155.00, 8.500],
['Krux', '8.0', 203.00, 8.000],
['Krux', '8.25', 210.00, 8.250],
['Krux', '8.5', 216.00, 8.500],
['Tensor', '5.25', 133.00, 8.000],
['Tensor', '5.5', 140.00, 8.125],
['Paris', '150mm', 150.00, 8.250],
['Paris', '180mm', 180.00, 8.750],
['Bear', '852', 216.00, 8.500],
['Bear', '840', 203.00, 8.000],
['Caliber', '44°', 184.00, 8.500],
['Caliber', '50°', 184.00, 8.500]
];
foreach ($truck_sizes as $truck) {
$wpdb->replace($table_name, [
'brand' => $truck[0],
'model' => $truck[1],
'hanger_width_mm' => $truck[2],
'axle_width_inches' => $truck[3]
]);
}
error_log('Product Machine: Sample truck sizes populated in WordPress database');
}
// Ensure truck sizes table exists (auto-create if needed)
function product_machine_ensure_truck_sizes_table() {
global $wpdb;
$table_name = $wpdb->prefix . 'product_machine_truck_sizes';
// Check if table exists
$table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name;
if (!$table_exists) {
// Create table
$sql = "CREATE TABLE $table_name (
id INT AUTO_INCREMENT PRIMARY KEY,
brand VARCHAR(100) NOT NULL,
model VARCHAR(100) NOT NULL,
hanger_width_mm DECIMAL(5,2) NOT NULL,
axle_width_inches DECIMAL(6,3) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_truck (brand, model)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
$wpdb->query($sql);
// Populate with comprehensive truck data including NHS and DLXSF
$truck_sizes = [
// ACE Trucks
['ACE', '44', 44.00, 8.000],
['ACE', '55', 55.00, 8.250],
['ACE', '66', 66.00, 8.500],
// Independent Trucks
['Independent', '139', 139.00, 7.875],
['Independent', '144', 144.00, 8.125],
['Independent', '149', 149.00, 8.375],
['Independent', '159', 159.00, 8.625],
['Independent', '169', 169.00, 8.875],
// Thunder Trucks
['Thunder', '143', 143.00, 8.000],
['Thunder', '147', 147.00, 8.250],
['Thunder', '151', 151.00, 8.500],
['Thunder', '161', 161.00, 8.750],
// Venture Trucks
['Venture', '5.0', 127.00, 7.750],
['Venture', '5.25', 133.00, 8.000],
['Venture', '5.8', 147.00, 8.250],
['Venture', '6.1', 155.00, 8.500],
// NHS (Natural High Skateboarding) Trucks
['NHS', '5.0', 127.00, 7.750],
['NHS', '5.25', 133.00, 8.000],
['NHS', '5.5', 140.00, 8.125],
['NHS', '5.8', 147.00, 8.250],
['NHS', '6.1', 155.00, 8.500],
// DLXSF (Deluxe San Francisco) Trucks
['DLXSF', '139', 139.00, 7.875],
['DLXSF', '144', 144.00, 8.125],
['DLXSF', '149', 149.00, 8.375],
['DLXSF', '159', 159.00, 8.625],
['DLXSF', '169', 169.00, 8.875],
// Other popular brands
['Krux', '8.0', 203.00, 8.000],
['Krux', '8.25', 210.00, 8.250],
['Krux', '8.5', 216.00, 8.500],
['Tensor', '5.25', 133.00, 8.000],
['Tensor', '5.5', 140.00, 8.125],
['Paris', '150mm', 150.00, 8.250],
['Paris', '180mm', 180.00, 8.750],
['Bear', '852', 216.00, 8.500],
['Bear', '840', 203.00, 8.000],
['Caliber', '44°', 184.00, 8.500],
['Caliber', '50°', 184.00, 8.500]
];
foreach ($truck_sizes as $truck) {
$wpdb->insert($table_name, [
'brand' => $truck[0],
'model' => $truck[1],
'hanger_width_mm' => $truck[2],
'axle_width_inches' => $truck[3]
]);
}
}
}
// Helper function to get truck sizes database connection (external or WordPress)
function pm_get_truck_sizes_db() {
$conn = pm_get_external_db_connection();
if ($conn) {
return ['type' => 'external', 'conn' => $conn, 'table' => 'truck_sizes'];
} else {
global $wpdb;
return ['type' => 'wordpress', 'conn' => $wpdb, 'table' => $wpdb->prefix . 'product_machine_truck_sizes'];
}
}
// Lookup truck axle width from reference table based on product description
function pm_lookup_truck_width($description) {
global $wpdb;
$table_name = $wpdb->prefix . 'product_machine_truck_sizes';
// Check if table exists
$table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name));
if ($table_exists !== $table_name) {
return null;
}
// Define truck brand patterns to match in description
$brands = ['ACE', 'Independent', 'Thunder', 'Venture', 'NHS', 'DLXSF', 'Krux', 'Tensor', 'Paris', 'Bear', 'Caliber'];
// Try to extract brand and model from description
foreach ($brands as $brand) {
// Case-insensitive brand match with flexible spacing
if (preg_match('/\b' . preg_quote($brand, '/') . '\b/i', $description, $brand_match)) {
// Common truck model patterns:
// ACE 44, ACE 55, ACE 66
// Independent 139, 144, 149, 159, 169
// Thunder 143, 147, 151, 161
// Venture 5.0, 5.25, 5.8, 6.1
// NHS 5.0, 5.25, 5.5, 5.8, 6.1
// DLXSF 139, 144, 149, 159, 169
// Krux 8.0, 8.25, 8.5
// Pattern: brand followed by optional "AF" or similar, then model number
if (preg_match('/\b' . preg_quote($brand, '/') . '\s*(?:AF[I1]\s*)?(\d{2,3}|\d\.\d{1,2})\b/i', $description, $model_match)) {
$model = $model_match[1];
// Query database for matching truck
$result = $wpdb->get_row($wpdb->prepare(
"SELECT axle_width_inches FROM $table_name WHERE LOWER(brand) = LOWER(%s) AND model = %s LIMIT 1",
$brand,
$model
));
if ($result && $result->axle_width_inches) {
// Format to remove unnecessary trailing zeros (8.000 -> 8.0, 8.250 -> 8.25)
$width = (float)$result->axle_width_inches;
// Use rtrim to remove trailing zeros, but keep at least one decimal place
$formatted = rtrim(rtrim(number_format($width, 3, '.', ''), '0'), '.');
// Ensure at least one decimal place
if (strpos($formatted, '.') === false) {
$formatted .= '.0';
}
return $formatted;
}
}
}
}
return null;
// Special-case: 'bearing' + 'lube/speed cream/grease' -> Lubricants
if (preg_match('/\bbearings?\b/', $name) && preg_match('/\b(lube|lubes|lubricant|lubricants|speed cream|speedcream|grease|wax|silicone)\b/', $name)) {
return [
'category_name' => 'Accessories > Lubricants',
'category_code' => 'LU'
];
}
}
// Register activation hook
register_activation_hook(__FILE__, 'product_machine_create_truck_sizes_table');
// Add admin action to initialize truck sizes table
add_action('wp_ajax_pm_init_truck_table', function() {
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
// Force creation of WordPress table since external DB is likely unavailable
global $wpdb;
$table_name = $wpdb->prefix . 'product_machine_truck_sizes';
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
id INT AUTO_INCREMENT PRIMARY KEY,
brand VARCHAR(100) NOT NULL,
model VARCHAR(100) NOT NULL,
hanger_width_mm DECIMAL(5,2) NOT NULL,
axle_width_inches DECIMAL(6,3) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_truck (brand, model)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
$result = $wpdb->query($sql);
if ($result !== false) {
// Add sample data
$truck_sizes = [
['ACE', '44', 44.00, 8.000],
['ACE', '55', 55.00, 8.250],
['Independent', '149', 149.00, 8.375],
['Thunder', '147', 147.00, 8.250],
['Venture', '5.25', 133.00, 8.000]
];
foreach ($truck_sizes as $truck) {
$wpdb->replace($table_name, [
'brand' => $truck[0],
'model' => $truck[1],
'hanger_width_mm' => $truck[2],
'axle_width_inches' => $truck[3]
]);
}
$count = $wpdb->get_var("SELECT COUNT(*) FROM $table_name");
wp_send_json_success(['message' => "Table created successfully with $count rows", 'count' => $count]);
} else {
wp_send_json_error(['message' => 'Table creation failed: ' . $wpdb->last_error]);
}
});
// Enqueue scripts and styles for the plugin
add_action('admin_enqueue_scripts', 'product_machine_admin_scripts');
add_action( 'admin_notices', function () {
global $pm_errors;
if ( empty( $pm_errors ) ) {
return;
}
echo '
Product-Machine debug:
';
foreach ( $pm_errors as $msg ) {
echo esc_html( $msg ) . '
';
}
echo '
';
// clear so we don’t print the same lines 10× on ajax-driven pages
$pm_errors = [];
} );
//dequeue elex because they suck
function dequeue_usps_shipping_js()
{
// Check if we're not on a WooCommerce or USPS settings page
if (!is_admin() || !isset($_GET['page']) || $_GET['page'] !== 'woocommerce_wf_shipping_usps_settings') {
wp_dequeue_script('wf_common_js'); // Replace with the correct handle of the script
}
}
add_action('admin_enqueue_scripts', 'dequeue_usps_shipping_js', 100);
// Conditionally dequeue the flexible shipping UPS script if it's loaded in the admin
add_action('admin_enqueue_scripts', function () {
if (isset($_GET['page']) && $_GET['page'] === 'product-machine-unpack-words') {
wp_dequeue_script('flexible-shipping-ups-blocks-integration-frontend');
}
}, 100);
// Force scripts to load in the footer on specific admin page
add_action('admin_enqueue_scripts', function () {
if (isset($_GET['page']) && $_GET['page'] === 'product-machine-unpack-words') {
global $wp_scripts;
foreach ($wp_scripts->queue as $handle) {
$wp_scripts->registered[$handle]->args = 1;
}
}
});
add_action('wp_print_scripts', function () {
if (isset($_GET['page']) && $_GET['page'] === 'product-machine-unpack-words') {
wp_deregister_script('flexible-shipping-ups-blocks-integration-frontend');
}
}, 100);
function forcefully_deregister_flexible_shipping_script()
{
wp_deregister_script('flexible-shipping-ups-blocks-integration-frontend');
}
add_action('wp_enqueue_scripts', 'forcefully_deregister_flexible_shipping_script', 100);
add_action('admin_enqueue_scripts', 'forcefully_deregister_flexible_shipping_script', 100);
add_action('login_enqueue_scripts', 'forcefully_deregister_flexible_shipping_script', 100);
// Fetch unpack words from the database (AJAX handler)
add_action('wp_ajax_pm_fetch_unpack_words', 'pm_fetch_unpack_words');
function pm_fetch_unpack_words()
{
check_ajax_referer('pm_ajax_nonce', 'nonce');
// Connect to the external database
$db = pm_get_external_db_connection();
if (!$db) {
wp_send_json_error('Database connection error: ' . mysqli_connect_error());
}
$results = [];
$query = "SELECT unpacking_word_ref_num, unpacking_word FROM product_machine_unpack_words";
$result = $db->query($query);
if ($result) {
while ($row = $result->fetch_assoc()) {
$results[] = $row;
}
// Log the results for debugging
error_log(print_r($results, true));
// Send the results directly without wrapping in 'data' key
wp_send_json_success($results);
} else {
wp_send_json_error('Error fetching data: ' . $db->error);
}
$db->close();
wp_die(); // End the AJAX request
}
// Delete an unpack word by ID (AJAX handler)
add_action('wp_ajax_pm_delete_unpack_word', 'pm_delete_unpack_word');
function pm_delete_unpack_word()
{
check_ajax_referer('pm_ajax_nonce', 'nonce');
if (!isset($_POST['id'])) {
wp_send_json_error('Invalid ID');
}
// Connect to the external database
$db = pm_get_external_db_connection();
if (!$db) {
wp_send_json_error('Database connection error: ' . mysqli_connect_error());
}
$id = intval($_POST['id']);
// Some tables use 'unpacking_word_ref_num' as primary key; support both
$query = "DELETE FROM product_machine_unpack_words WHERE unpacking_word_ref_num = ? OR id = ?";
$stmt = $db->prepare($query);
$stmt->bind_param('ii', $id, $id);
$stmt->execute();
if ($stmt->affected_rows > 0) {
wp_send_json_success();
} else {
$err = $stmt->error ?: $db->error ?: 'Unknown error';
error_log('pm_delete_unpack_word: delete failed: ' . $err);
wp_send_json_error('Failed to delete record: ' . $err);
}
$stmt->close();
$db->close();
wp_die(); // End the AJAX request
}
// Save (create or update) an unpack word (AJAX handler)
add_action('wp_ajax_pm_save_unpack_word', 'pm_save_unpack_word');
function pm_save_unpack_word()
{
check_ajax_referer('pm_ajax_nonce', 'nonce');
if (!isset($_POST['unpack_word'])) {
wp_send_json_error('Missing unpack_word parameter');
}
$unpack_word = sanitize_text_field($_POST['unpack_word']);
if ($unpack_word === '') {
wp_send_json_error('Unpack word cannot be empty');
}
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
$db = pm_get_external_db_connection();
if (!$db) {
wp_send_json_error('Database connection error: ' . mysqli_connect_error());
}
if ($id > 0) {
$query = "UPDATE product_machine_unpack_words SET unpacking_word = ? WHERE unpacking_word_ref_num = ? OR id = ?";
$stmt = $db->prepare($query);
if (!$stmt) { wp_send_json_error('Prepare failed: ' . $db->error); }
$stmt->bind_param('sii', $unpack_word, $id, $id);
$stmt->execute();
if ($stmt->affected_rows >= 0) {
wp_send_json_success();
} else {
wp_send_json_error('Failed to update record');
}
$stmt->close();
$db->close();
wp_die();
}
// Insert new unpack word
// Include unpacking_word_selling_quan with a reasonable default (1) to avoid missing-default DB errors
$query = "INSERT INTO product_machine_unpack_words (unpacking_word, unpacking_word_replace, unpacking_word_received_quan, unpacking_word_selling_quan) VALUES (?, '', 1, 1)";
$stmt = $db->prepare($query);
if (!$stmt) { error_log('pm_save_unpack_word: prepare failed: ' . $db->error); wp_send_json_error('Prepare failed: ' . $db->error); }
$stmt->bind_param('s', $unpack_word);
$stmt->execute();
if ($stmt->affected_rows > 0) {
wp_send_json_success(['insert_id' => $db->insert_id]);
} else {
$err = $stmt->error ?: $db->error ?: 'Unknown error';
error_log('pm_save_unpack_word: insert failed: ' . $err);
error_log('pm_save_unpack_word: insert failed, attempting comprehensive schema-aware fallback: ' . $err);
// Attempt a comprehensive schema-aware fallback: include any NOT NULL columns without defaults
$cols_res = $db->query("SHOW COLUMNS FROM product_machine_unpack_words");
$required_cols = ['unpacking_word', 'unpacking_word_replace', 'unpacking_word_received_quan'];
$values = [];
// seed values for the default columns
$values['unpacking_word'] = $unpack_word;
$values['unpacking_word_replace'] = '';
$values['unpacking_word_received_quan'] = 1;
if ($cols_res) {
while ($col = $cols_res->fetch_assoc()) {
$field = $col['Field'];
// skip auto-increment PKs and columns we already have
if (isset($values[$field])) { continue; }
if (strpos($col['Extra'], 'auto_increment') !== false) { continue; }
// If column is NOT NULL and has no default, we must supply a value
$is_not_null = (strtoupper($col['Null']) === 'NO');
$has_default = !is_null($col['Default']);
if ($is_not_null && !$has_default) {
// choose sensible default based on type
$type = $col['Type'];
if (preg_match('/int|decimal|float|double|numeric/i', $type)) {
$values[$field] = 1;
} elseif (preg_match('/date|time|timestamp/i', $type)) {
$values[$field] = '1970-01-01';
} else {
$values[$field] = '';
}
}
}
}
// If we found any extra required columns, build a prepared INSERT with placeholders for all columns we will write
if (count($values) > 0) {
error_log('pm_save_unpack_word: comprehensive fallback will write columns: ' . json_encode(array_keys($values)) . ' values: ' . json_encode(array_values($values)));
$cols = array_keys($values);
$placeholders = array_fill(0, count($cols), '?');
$insert_query = "INSERT INTO product_machine_unpack_words (" . implode(', ', $cols) . ") VALUES (" . implode(', ', $placeholders) . ")";
$fallback_stmt = $db->prepare($insert_query);
if ($fallback_stmt) {
// Build types and bind values
$types = '';
$bind_vals = [];
foreach ($cols as $c) {
$v = $values[$c];
if (is_int($v)) { $types .= 'i'; }
else { $types .= 's'; }
$bind_vals[] = $v;
}
// prepare bind_param arguments
$bind_names = [];
$bind_names[] = & $types;
foreach ($bind_vals as $k => $v) {
$bind_names[] = & $bind_vals[$k];
}
call_user_func_array([$fallback_stmt, 'bind_param'], $bind_names);
$fallback_stmt->execute();
if ($fallback_stmt->affected_rows > 0) {
wp_send_json_success(['insert_id' => $db->insert_id, 'fallback' => true]);
} else {
error_log('pm_save_unpack_word: comprehensive fallback prepare/execute failed: ' . ($fallback_stmt->error ?: $db->error));
}
} else {
error_log('pm_save_unpack_word: failed to prepare comprehensive fallback: ' . $db->error);
}
}
// As a last resort, attempt a direct INSERT with minimal columns we control
$escaped_word = $db->real_escape_string($unpack_word);
$fallback_query = "INSERT INTO product_machine_unpack_words (unpacking_word, unpacking_word_replace, unpacking_word_received_quan) VALUES ('" . $escaped_word . "', '', 1)";
if ($db->query($fallback_query)) {
error_log('pm_save_unpack_word: direct fallback insert succeeded, id=' . $db->insert_id);
wp_send_json_success(['insert_id' => $db->insert_id, 'fallback' => true, 'direct' => true]);
} else {
error_log('pm_save_unpack_word: direct fallback insert failed: ' . $db->error);
}
// If duplicate entry, try to locate existing row and return it
if (stripos($err, 'duplicate') !== false || stripos($err, 'Duplicate') !== false) {
$search = $db->real_escape_string($unpack_word);
$res = $db->query("SELECT unpacking_word_ref_num AS id FROM product_machine_unpack_words WHERE unpacking_word = '" . $search . "' LIMIT 1");
if ($res && $row = $res->fetch_assoc()) {
wp_send_json_success(['insert_id' => intval($row['id']), 'duplicate' => true]);
}
wp_send_json_error('Duplicate record exists but could not fetch it.');
}
wp_send_json_error('Failed to insert record: ' . $err);
}
$stmt->close();
$db->close();
wp_die();
}
// Handle AJAX request for data preview
add_action('wp_ajax_pm_preview_data', 'pm_preview_data');
// Lightweight diagnostic endpoint to test admin-ajax reachability
add_action('wp_ajax_pm_diag', function () {
check_ajax_referer('pm_ajax_nonce', 'nonce');
$info = [
'php' => phpversion(),
'wp' => get_bloginfo('version'),
'time' => date('c'),
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'loaded_extensions' => array_slice(get_loaded_extensions(), 0, 10),
];
wp_send_json_success(['diag' => $info]);
});
// Early fatal catcher for preview requests (runs before handler)
add_action('admin_init', function () {
if (!is_admin()) { return; }
if (!isset($_POST['action']) || $_POST['action'] !== 'pm_preview_data') { return; }
// Install a shutdown handler to emit JSON if something fatal happens before pm_preview_data executes
$start = microtime(true);
register_shutdown_function(function () use ($start) {
$e = error_get_last();
if ($e && in_array($e['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
while (ob_get_level() > 0) { @ob_end_clean(); }
if (!headers_sent()) {
header('Content-Type: application/json; charset=utf-8');
}
echo wp_json_encode([
'success' => false,
'data' => [
'message' => 'Fatal before handler',
'fatal' => $e,
'diag' => [
'duration_ms' => round((microtime(true) - $start) * 1000, 2)
]
]
]);
exit;
}
});
});
/*
* Admin product list search: include direct_distributor_sku and wholesaler_sku in the search
* without affecting default title/content search. Filters are limited to the admin products list.
*/
add_filter('posts_join', function ($join, $query) {
global $pagenow, $wpdb;
if (!is_admin() || $pagenow !== 'edit.php') return $join;
if (!isset($query->query['post_type']) || $query->query['post_type'] !== 'product') return $join;
if (empty($query->query['s'])) return $join;
// Join postmeta table for the SKU meta keys so we can search against them
$join .= " LEFT JOIN {$wpdb->postmeta} pm_sku ON ({$wpdb->posts}.ID = pm_sku.post_id AND pm_sku.meta_key IN ('_sku','wholesaler_sku','direct_distributor_sku'))";
return $join;
}, 10, 2);
add_filter('posts_where', function ($where, $query) {
global $pagenow, $wpdb;
if (!is_admin() || $pagenow !== 'edit.php') return $where;
if (!isset($query->query['post_type']) || $query->query['post_type'] !== 'product') return $where;
if (empty($query->query['s'])) return $where;
$q = $wpdb->esc_like($query->query['s']);
$like = "%{$q}%";
// Keep the existing title/content search, but OR in meta value searches
$where .= $wpdb->prepare(" OR pm_sku.meta_value LIKE %s", $like);
return $where;
}, 10, 2);
add_filter('posts_distinct', function ($distinct, $query) {
global $pagenow;
if (!is_admin() || $pagenow !== 'edit.php') return $distinct;
if (!isset($query->query['post_type']) || $query->query['post_type'] !== 'product') return $distinct;
if (empty($query->query['s'])) return $distinct;
return 'DISTINCT';
}, 10, 2);
// REST API fallback for preview to avoid admin-ajax proxies/WAF blocking
add_action('rest_api_init', function () {
register_rest_route('product-machine/v1', '/preview', [
'methods' => 'POST',
'permission_callback' => function () {
return current_user_can('manage_options');
},
'callback' => function (WP_REST_Request $request) {
// Start output buffering to prevent any notices/warnings from breaking JSON
ob_start();
// CRITICAL: Check database connection FIRST
global $wpdb;
if (!$wpdb || $wpdb->last_error) {
ob_end_clean();
return new WP_REST_Response([
'success' => false,
'data' => [
'message' => '🚨 DATABASE CONNECTION FAILED - Cannot process preview without database access',
'error' => $wpdb ? $wpdb->last_error : 'WordPress database object is null',
'db_host' => defined('DB_HOST') ? DB_HOST : 'undefined'
]
], 500);
}
// Test a simple query to verify DB is actually working
$test_query = $wpdb->get_var("SELECT 1");
if ($test_query !== '1') {
ob_end_clean();
return new WP_REST_Response([
'success' => false,
'data' => [
'message' => '🚨 DATABASE NOT RESPONDING - Query test failed',
'error' => $wpdb->last_error ?: 'Query returned: ' . var_export($test_query, true)
]
], 500);
}
$raw_data = (string) $request->get_param('raw_data');
$invoice = (string) $request->get_param('invoicename');
$sku_usage = (string) $request->get_param('sku_usage');
$debug = (int) $request->get_param('debug');
$lite = (bool) $request->get_param('lite');
if ($raw_data === '') {
ob_end_clean();
return new WP_REST_Response(['success' => false, 'data' => ['message' => 'No data provided.']], 400);
}
try {
$products = product_machine_process_raw_data($raw_data, $invoice, $sku_usage, $lite);
} catch (Throwable $t) {
ob_end_clean();
return new WP_REST_Response([
'success' => false,
'data' => [
'message' => 'Processing exception',
'throwable' => [
'type' => get_class($t), 'msg' => $t->getMessage(), 'file' => $t->getFile(), 'line' => $t->getLine()
]
]
], 500);
}
$debug_log = '';
if ($debug) {
$debug_log = pm_get_last_lines_of_file(WP_CONTENT_DIR . '/debug.log', 100, 'product-machine');
}
// Discard any unexpected output
ob_end_clean();
return new WP_REST_Response([
'success' => true,
'data' => [
'products' => $products,
'errors' => [],
'debug_log' => $debug_log
]
], 200);
}
]);
// Import one row idempotently by invoice and line
register_rest_route('product-machine/v1', '/import-one', [
'methods' => 'POST',
'permission_callback' => function () { return current_user_can('manage_options'); },
'callback' => function (WP_REST_Request $request) {
// CRITICAL: Check database connection FIRST (same behavior as preview route)
global $wpdb;
if (!$wpdb || $wpdb->last_error) {
return new WP_REST_Response([
'success' => false,
'data' => [
'message' => '🚨 DATABASE CONNECTION FAILED - Cannot import without database access',
'error' => $wpdb ? $wpdb->last_error : 'WordPress database object is null',
'db_host' => defined('DB_HOST') ? DB_HOST : 'undefined'
]
], 500);
}
// Basic DB sanity check
$test_query = $wpdb->get_var("SELECT 1");
if ($test_query !== '1') {
return new WP_REST_Response([
'success' => false,
'data' => [
'message' => '🚨 DATABASE NOT RESPONDING - Query test failed',
'error' => $wpdb->last_error ?: 'Query returned: ' . var_export($test_query, true)
]
], 500);
}
$invoice = (string) $request->get_param('invoice');
$line = (string) $request->get_param('line'); // e.g. 006
$name = (string) $request->get_param('name');
$sku = (string) $request->get_param('sku');
$price = (string) $request->get_param('price');
$cost = (string) $request->get_param('cost');
$qty = (int) $request->get_param('qty');
$category = (string) $request->get_param('category');
$brand = (string) $request->get_param('brand');
if ($invoice === '' || $line === '') {
return new WP_REST_Response(['success' => false, 'data' => ['message' => 'Missing invoice or line']], 400);
}
// Build a deterministic meta key and SKU fallback for idempotency
$invoice_line_id = $invoice . '-' . str_pad($line, 3, '0', STR_PAD_LEFT);
// If no SKU, generate a deterministic base using known data
if ($sku === '') {
$brand_code = get_brand_code($brand);
$category_code = '';
$cat_info = find_product_category($name);
if (is_array($cat_info)) { $category_code = $cat_info['category_code'] ?? ''; }
$sku = generate_sku($category_code, $brand_code, $name);
}
// Check if a product with this invoice_line id already exists
$existing = get_posts([
'post_type' => 'product',
'meta_key' => 'pm_invoice_line',
'meta_value' => $invoice_line_id,
'posts_per_page' => 1,
'fields' => 'ids'
]);
if (!empty($existing)) {
$product_id = (int) $existing[0];
$product = wc_get_product($product_id);
} else {
// Try by SKU next (in case prior imports used SKU instead of meta).
// Use pm_get_product_id_by_any_sku so we also look for wholesaler/distributor SKUs.
$product_id = pm_get_product_id_by_any_sku($sku);
if ($product_id) {
$product = wc_get_product($product_id);
} else {
$product = new WC_Product();
}
}
// Apply updates
if ($name !== '') { $product->set_name($name); $product->set_description($name); }
if ($price !== '') { $product->set_price($price); $product->set_regular_price($price); }
if ($sku !== '') { $product->set_sku($sku); }
// Enable stock management and set quantity
$product->set_manage_stock(true);
if ($qty > 0) {
$product->set_stock_quantity($qty);
$product->set_stock_status('instock');
} else {
$product->set_stock_quantity(0);
$product->set_stock_status('outofstock');
}
// Categories - handle hierarchical categories
if ($category !== '') {
$term_id = create_hierarchical_category($category);
if ($term_id) {
$product->set_category_ids([$term_id]);
}
}
// Attributes handled below via taxonomy-aware merge
// Attributes - merge with existing attributes using taxonomy when available
try {
$brand = isset($request['brand']) ? sanitize_text_field($request['brand']) : '';
$width_in = isset($request['width_in']) ? sanitize_text_field($request['width_in']) : '';
$size = isset($request['size']) ? sanitize_text_field($request['size']) : '';
$durometer = isset($request['durometer']) ? sanitize_text_field($request['durometer']) : '';
$existing_attrs = $product->get_attributes();
if (!is_array($existing_attrs)) { $existing_attrs = []; }
// Helper to upsert a taxonomy attribute (or custom if taxonomy missing)
$upsert_attr = function($attrs, $tax_slug, $label, $value) {
if ($value === '' || $value === null) return $attrs;
$is_tax = taxonomy_exists($tax_slug);
$attr_obj = new WC_Product_Attribute();
if ($is_tax) {
// Ensure term exists
$term = get_term_by('name', $value, $tax_slug);
if (!$term || is_wp_error($term)) {
$ins = wp_insert_term($value, $tax_slug);
if (!is_wp_error($ins) && isset($ins['term_id'])) {
$term_id = (int)$ins['term_id'];
} else {
// fallback: cannot create term, treat as custom
$is_tax = false;
}
}
if ($is_tax) {
$term = get_term_by('name', $value, $tax_slug); // refresh
$term_id = $term && !is_wp_error($term) ? (int)$term->term_id : 0;
$attr_obj->set_id(wc_attribute_taxonomy_id_by_name(str_replace('pa_', '', $tax_slug)) ?: 0);
$attr_obj->set_name($tax_slug);
$attr_obj->set_options($term_id ? [$term_id] : []);
$attr_obj->set_visible(true);
$attr_obj->set_variation(false);
$attr_obj->set_position(0);
// Merge: replace if exists by slug key, else add
$attrs[$tax_slug] = $attr_obj;
return $attrs;
}
}
// Custom attribute fallback
$attr_obj->set_id(0);
$attr_obj->set_name($label);
$attr_obj->set_options([$value]);
$attr_obj->set_visible(true);
$attr_obj->set_variation(false);
$attr_obj->set_position(0);
// Merge: replace by lowercase label key
$attrs[strtolower($label)] = $attr_obj;
return $attrs;
};
// Upsert Brand and Width
if (!empty($brand)) {
$existing_attrs = $upsert_attr($existing_attrs, 'pa_brand', 'Brand', $brand);
}
if (!empty($width_in)) {
$existing_attrs = $upsert_attr($existing_attrs, 'pa_width', 'Width', $width_in);
}
// Upsert Size (wheel diameter) and Durometer when present
if (!empty($size)) {
$existing_attrs = $upsert_attr($existing_attrs, 'pa_wheel-diameter', 'Size', $size);
}
if (!empty($durometer)) {
$existing_attrs = $upsert_attr($existing_attrs, 'pa_durometer', 'Durometer', $durometer);
}
if (!empty($existing_attrs)) {
$product->set_attributes($existing_attrs);
}
} catch (Throwable $e) {
// keep going; attributes are best-effort
}
// Save and add invoice meta for idempotency
try {
$product_id = $product->save();
} catch (Throwable $e) {
return new WP_REST_Response(['success' => false, 'data' => ['message' => 'Save failed', 'exception' => $e->getMessage()]], 500);
}
update_post_meta($product_id, 'pm_invoice', $invoice);
update_post_meta($product_id, 'pm_invoice_line', $invoice_line_id);
// Also save to ACF fields 'invoice' and 'invoice_line_number' if ACF is active (dual-field sync)
if (function_exists('update_field')) {
// Write base invoice ID to ACF 'invoice' field
update_field('invoice', $invoice, $product_id);
// Also try by field key if name mapping is not present
@update_field('field_6339fa1d40f53', $invoice, $product_id);
// Write full invoice line number to ACF 'invoice_line_number' field
update_field('invoice_line_number', $invoice_line_id, $product_id);
// Also try by field key if name mapping is not present
@update_field('field_6339f9f540f52', $invoice_line_id, $product_id);
}
// Ensure raw meta is set so ACF can read values even if field key missing (dual-field guarantee)
update_post_meta($product_id, 'invoice', $invoice);
update_post_meta($product_id, '_invoice', 'field_6339fa1d40f53');
update_post_meta($product_id, 'invoice_line_number', $invoice_line_id);
update_post_meta($product_id, '_invoice_line_number', 'field_6339f9f540f52');
// Update cost meta fields: ACF 'cost' and COGS plugin meta '_alg_wc_cog_cost'
if ($cost !== '') {
// ACF field write (by name; field key fallback optional if known)
if (function_exists('update_field')) {
@update_field('cost', $cost, $product_id);
}
// Raw post meta for ACF value
update_post_meta($product_id, 'cost', $cost);
// Common COGS plugin field
update_post_meta($product_id, '_alg_wc_cog_cost', $cost);
// Optional fallback some themes/plugins read
@update_post_meta($product_id, '_cost', $cost);
}
// Build a normalized attributes string from the saved product and compare with expected
$current_attributes_str = pm_build_current_attributes_string($product);
// Build expected attributes string from input values (presence-based)
$expected_pairs = [];
if (!empty($brand)) { $expected_pairs[] = 'Brand: ' . $brand; }
if (!empty($width_in)) {
$w = rtrim($width_in, '"');
if (strpos($w, '"') === false) { $w = $w . '"'; }
$expected_pairs[] = 'Width: ' . $w;
}
if (!empty($size)) { $expected_pairs[] = 'Size: ' . $size; }
if (!empty($durometer)) { $expected_pairs[] = 'Durometer: ' . $durometer; }
$expected_attributes_str = implode(', ', $expected_pairs);
$attributes_match = pm_attributes_match($expected_attributes_str, $current_attributes_str);
return new WP_REST_Response(['success' => true, 'data' => [
'product_id' => $product_id,
'sku' => $product->get_sku(),
'invoice' => $invoice,
'invoice_line' => $invoice_line_id,
'edit_link' => get_edit_post_link($product_id),
'product_title' => get_the_title($product_id),
'current_attributes' => $current_attributes_str,
'expected_attributes' => $expected_attributes_str,
'attributes_match' => $attributes_match
]], 200);
}
]);
// Cleanup orphaned categories endpoint
register_rest_route('product-machine/v1', '/cleanup-categories', [
'methods' => 'POST',
'permission_callback' => function () { return current_user_can('manage_options'); },
'callback' => function (WP_REST_Request $request) {
$cleaned_up = cleanup_orphaned_categories();
return new WP_REST_Response(['success' => true, 'data' => [
'cleaned_up' => $cleaned_up,
'message' => 'Category cleanup completed'
]], 200);
}
]);
// CRUD endpoints for Manage Products GUI
// List all products
register_rest_route('product-machine/v1', '/products', [
'methods' => 'GET',
'permission_callback' => function () { return current_user_can('manage_options'); },
'callback' => function (WP_REST_Request $request) {
// Quick DB sanity check
global $wpdb;
if (!$wpdb || $wpdb->last_error) {
return new WP_REST_Response(['success' => false, 'data' => ['message' => 'Database unavailable']], 500);
}
$args = [
'post_type' => 'product',
'posts_per_page' => -1,
'post_status' => 'any'
];
$products_query = new WP_Query($args);
$products = [];
foreach ($products_query->posts as $post) {
$product = wc_get_product($post->ID);
if (!$product) continue;
$products[] = [
'id' => $post->ID,
'sku' => $product->get_sku(),
'name' => $product->get_name(),
'invoice_line' => get_post_meta($post->ID, 'pm_invoice_line', true),
'price' => $product->get_price(),
'stock' => $product->get_stock_quantity(),
'category' => implode(', ', wp_get_post_terms($post->ID, 'product_cat', ['fields' => 'names'])),
'brand' => get_post_meta($post->ID, '_wc_brand', true) // or from attributes
];
}
return new WP_REST_Response(['success' => true, 'data' => ['products' => $products]], 200);
}
]);
// Get single product
register_rest_route('product-machine/v1', '/products/(?P\d+)', [
'methods' => 'GET',
'Categories' => $product_data['Categories'] ?? '',
'Category_Code' => $product_data['category_code'] ?? '',
'callback' => function (WP_REST_Request $request) {
$product_id = (int) $request['id'];
$product = wc_get_product($product_id);
if (!$product) {
return new WP_REST_Response(['success' => false, 'data' => ['message' => 'Product not found']], 404);
}
$invoice_line = get_post_meta($product_id, 'pm_invoice_line', true);
$invoice = get_post_meta($product_id, 'pm_invoice', true);
$line_parts = explode('-', $invoice_line);
return new WP_REST_Response(['success' => true, 'data' => ['product' => [
'id' => $product_id,
'sku' => $product->get_sku(),
'name' => $product->get_name(),
'invoice' => $invoice,
'line' => count($line_parts) > 1 ? end($line_parts) : '',
'price' => $product->get_price(),
'stock' => $product->get_stock_quantity(),
'category' => implode(', ', wp_get_post_terms($product_id, 'product_cat', ['fields' => 'names'])),
'brand' => get_post_meta($product_id, '_wc_brand', true)
]]], 200);
}
]);
// Create product
register_rest_route('product-machine/v1', '/products', [
'methods' => 'POST',
'permission_callback' => function () { return current_user_can('manage_options'); },
'callback' => function (WP_REST_Request $request) {
$sku = sanitize_text_field($request->get_param('sku'));
$name = sanitize_text_field($request->get_param('name'));
$price = (float) $request->get_param('price');
$qty = (int) $request->get_param('qty');
$category = sanitize_text_field($request->get_param('category'));
$brand = sanitize_text_field($request->get_param('brand'));
$invoice = sanitize_text_field($request->get_param('invoice'));
$line = sanitize_text_field($request->get_param('line'));
$cost = sanitize_text_field($request->get_param('cost'));
if (empty($sku) || empty($name)) {
return new WP_REST_Response(['success' => false, 'data' => ['message' => 'SKU and Name are required']], 400);
}
// Check if SKU already exists in any of the SKU fields (standard/wholesaler/distributor)
if (pm_get_product_id_by_any_sku($sku)) {
return new WP_REST_Response(['success' => false, 'data' => ['message' => 'SKU already exists']], 400);
}
$product = new WC_Product();
$product->set_sku($sku);
$product->set_name($name);
$product->set_description($name);
$product->set_price($price);
$product->set_regular_price($price);
$product->set_stock_quantity($qty);
if ($category) {
$term = term_exists($category, 'product_cat');
if (!$term) $term = wp_insert_term($category, 'product_cat');
if (!is_wp_error($term)) {
$product->set_category_ids([(int) ($term['term_id'] ?? $term->term_id ?? 0)]);
}
}
$product_id = $product->save();
if ($invoice && $line) {
$invoice_line_id = $invoice . '-' . str_pad($line, 3, '0', STR_PAD_LEFT);
update_post_meta($product_id, 'pm_invoice', $invoice);
update_post_meta($product_id, 'pm_invoice_line', $invoice_line_id);
// Also save to ACF field 'invoice_line_number' if ACF is active
if (function_exists('update_field')) {
update_field('invoice_line_number', $invoice_line_id, $product_id);
// Also ensure the base invoice ACF field is set
update_field('invoice', $invoice, $product_id);
@update_field('field_6339fa1d40f53', $invoice, $product_id);
@update_field('field_6339f9f540f52', $invoice_line_id, $product_id);
}
// Ensure raw postmeta is present as fallback for ACF
update_post_meta($product_id, 'invoice', $invoice);
update_post_meta($product_id, '_invoice', 'field_6339fa1d40f53');
update_post_meta($product_id, 'invoice_line_number', $invoice_line_id);
update_post_meta($product_id, '_invoice_line_number', 'field_6339f9f540f52');
}
// Save optionally-supplied SKU-variant meta if present
$direct_distributor_sku = sanitize_text_field($request->get_param('direct_distributor_sku'));
$wholesaler_sku = sanitize_text_field($request->get_param('wholesaler_sku'));
if (!empty($direct_distributor_sku)) {
update_post_meta($product_id, 'direct_distributor_sku', $direct_distributor_sku);
if (function_exists('update_field')) { update_field('direct_distributor_sku', $direct_distributor_sku, $product_id); }
}
if (!empty($wholesaler_sku)) {
update_post_meta($product_id, 'wholesaler_sku', $wholesaler_sku);
if (function_exists('update_field')) { update_field('wholesaler_sku', $wholesaler_sku, $product_id); }
}
if ($brand) {
update_post_meta($product_id, '_wc_brand', $brand);
}
// Dual-field cost update if present
if (!empty($cost)) {
if (function_exists('update_field')) {
@update_field('cost', $cost, $product_id);
}
update_post_meta($product_id, 'cost', $cost);
update_post_meta($product_id, '_alg_wc_cog_cost', $cost);
@update_post_meta($product_id, '_cost', $cost);
}
return new WP_REST_Response(['success' => true, 'data' => ['product_id' => $product_id]], 200);
}
]);
// Update product (excludes qty/stock)
register_rest_route('product-machine/v1', '/products/(?P\d+)', [
'methods' => 'PUT',
'permission_callback' => function () { return current_user_can('manage_options'); },
'callback' => function (WP_REST_Request $request) {
// Quick DB sanity check
global $wpdb;
if (!$wpdb || $wpdb->last_error) {
return new WP_REST_Response(['success' => false, 'data' => ['message' => 'Database unavailable']], 500);
}
$product_id = (int) $request['id'];
$product = wc_get_product($product_id);
if (!$product) {
return new WP_REST_Response(['success' => false, 'data' => ['message' => 'Product not found']], 404);
}
$sku = sanitize_text_field($request->get_param('sku'));
$name = sanitize_text_field($request->get_param('name'));
$price = (float) $request->get_param('price');
$category = sanitize_text_field($request->get_param('category'));
$brand = sanitize_text_field($request->get_param('brand'));
$invoice = sanitize_text_field($request->get_param('invoice'));
$line = sanitize_text_field($request->get_param('line'));
$cost = sanitize_text_field($request->get_param('cost'));
if ($sku) $product->set_sku($sku);
if ($name) {
$product->set_name($name);
$product->set_description($name);
}
if ($price) {
$product->set_price($price);
$product->set_regular_price($price);
}
// NOTE: qty is NOT updated on edit mode per requirements
if ($category) {
$term = term_exists($category, 'product_cat');
if (!$term) $term = wp_insert_term($category, 'product_cat');
if (!is_wp_error($term)) {
$product->set_category_ids([(int) ($term['term_id'] ?? $term->term_id ?? 0)]);
}
}
$product->save();
if ($invoice && $line) {
$invoice_line_id = $invoice . '-' . str_pad($line, 3, '0', STR_PAD_LEFT);
update_post_meta($product_id, 'pm_invoice', $invoice);
update_post_meta($product_id, 'pm_invoice_line', $invoice_line_id);
// Also save to ACF field 'invoice_line_number' if ACF is active
if (function_exists('update_field')) {
update_field('invoice_line_number', $invoice_line_id, $product_id);
// Keep the base invoice ACF field in sync as well
update_field('invoice', $invoice, $product_id);
@update_field('field_6339fa1d40f53', $invoice, $product_id);
@update_field('field_6339f9f540f52', $invoice_line_id, $product_id);
}
// Fallback raw postmeta to keep ACF in sync
update_post_meta($product_id, 'invoice', $invoice);
update_post_meta($product_id, '_invoice', 'field_6339fa1d40f53');
update_post_meta($product_id, 'invoice_line_number', $invoice_line_id);
update_post_meta($product_id, '_invoice_line_number', 'field_6339f9f540f52');
}
if ($brand) {
update_post_meta($product_id, '_wc_brand', $brand);
}
// Dual-field cost update if present
if (!empty($cost)) {
if (function_exists('update_field')) {
@update_field('cost', $cost, $product_id);
}
update_post_meta($product_id, 'cost', $cost);
update_post_meta($product_id, '_alg_wc_cog_cost', $cost);
@update_post_meta($product_id, '_cost', $cost);
}
return new WP_REST_Response(['success' => true, 'data' => ['product_id' => $product_id]], 200);
}
]);
// Delete product
register_rest_route('product-machine/v1', '/products/(?P\d+)', [
'methods' => 'DELETE',
'permission_callback' => function () { return current_user_can('manage_options'); },
'callback' => function (WP_REST_Request $request) {
$product_id = (int) $request['id'];
$product = wc_get_product($product_id);
if (!$product) {
return new WP_REST_Response(['success' => false, 'data' => ['message' => 'Product not found']], 404);
}
$product->delete(true); // force delete
return new WP_REST_Response(['success' => true, 'data' => ['message' => 'Product deleted']], 200);
}
]);
// Get truck sizes
register_rest_route('product-machine/v1', '/truck-sizes', [
'methods' => WP_REST_Server::READABLE,
'permission_callback' => function () { return current_user_can('manage_options'); },
'callback' => function () {
global $wpdb;
$table_name = $wpdb->prefix . 'product_machine_truck_sizes';
$exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name));
if ($exists !== $table_name) {
return new WP_REST_Response(['success' => true, 'data' => []], 200);
}
$rows = $wpdb->get_results("SELECT id, brand, model, hanger_width_mm, axle_width_inches, created_at, updated_at FROM $table_name ORDER BY brand, model", ARRAY_A);
if ($wpdb->last_error) {
return new WP_REST_Response([
'success' => false,
'data' => ['message' => 'Database error: ' . $wpdb->last_error]
], 500);
}
$truck_sizes = array_map(function ($row) {
return [
'id' => (int)$row['id'],
'brand' => $row['brand'],
'model' => $row['model'],
'hanger_width_mm' => (float)$row['hanger_width_mm'],
'axle_width_inches' => (float)$row['axle_width_inches'],
'created_at' => $row['created_at'],
'updated_at' => $row['updated_at'],
];
}, $rows ?: []);
return new WP_REST_Response(['success' => true, 'data' => $truck_sizes], 200);
}
]);
// Get single truck size
register_rest_route('product-machine/v1', '/truck-sizes/(?P\d+)', [
'methods' => 'GET',
'permission_callback' => function () { return current_user_can('manage_options'); },
'callback' => function (WP_REST_Request $request) {
global $wpdb;
$id = (int) $request['id'];
$table_name = $wpdb->prefix . 'product_machine_truck_sizes';
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $id), ARRAY_A);
if (!$row) {
return new WP_REST_Response(['success' => false, 'data' => ['message' => 'Truck size not found']], 404);
}
$truck_size = [
'id' => (int)$row['id'],
'brand' => $row['brand'],
'model' => $row['model'],
'hanger_width_mm' => (float)$row['hanger_width_mm'],
'axle_width_inches' => (float)$row['axle_width_inches'],
'created_at' => $row['created_at'],
'updated_at' => $row['updated_at']
];
return new WP_REST_Response(['success' => true, 'data' => $truck_size], 200);
}
]);
// Create truck size
register_rest_route('product-machine/v1', '/truck-sizes', [
'methods' => WP_REST_Server::CREATABLE,
'permission_callback' => function () { return current_user_can('manage_options'); },
'callback' => function (WP_REST_Request $request) {
global $wpdb;
$params = $request->get_json_params();
$required = ['brand', 'model', 'hanger_width_mm', 'axle_width_inches'];
foreach ($required as $field) {
if (!isset($params[$field]) || $params[$field] === '') {
return new WP_REST_Response([
'success' => false,
'data' => ['message' => sprintf('Missing required field: %s', $field)]
], 400);
}
}
if (!is_numeric($params['hanger_width_mm']) || !is_numeric($params['axle_width_inches'])) {
return new WP_REST_Response([
'success' => false,
'data' => ['message' => 'Widths must be numeric values']
], 400);
}
$table_name = $wpdb->prefix . 'product_machine_truck_sizes';
$axle_width = round((float)$params['axle_width_inches'] * 8) / 8;
$result = $wpdb->insert($table_name, [
'brand' => sanitize_text_field($params['brand']),
'model' => sanitize_text_field($params['model']),
'hanger_width_mm' => round((float)$params['hanger_width_mm'], 2),
'axle_width_inches' => $axle_width
], ['%s', '%s', '%f', '%f']);
if (!$result) {
$message = $wpdb->last_error ?: 'Unknown database error';
if (stripos($message, 'duplicate') !== false) {
return new WP_REST_Response([
'success' => false,
'data' => ['message' => 'Truck size already exists for that brand/model']
], 409);
}
return new WP_REST_Response([
'success' => false,
'data' => ['message' => 'Failed to create truck size: ' . $message]
], 500);
}
return new WP_REST_Response([
'success' => true,
'data' => ['id' => $wpdb->insert_id]
], 201);
}
]);
// Update truck size
register_rest_route('product-machine/v1', '/truck-sizes/(?P\d+)', [
'methods' => WP_REST_Server::EDITABLE,
'permission_callback' => function () { return current_user_can('manage_options'); },
'callback' => function (WP_REST_Request $request) {
global $wpdb;
$id = (int) $request['id'];
$params = $request->get_json_params();
$required = ['brand', 'model', 'hanger_width_mm', 'axle_width_inches'];
foreach ($required as $field) {
if (!isset($params[$field]) || $params[$field] === '') {
return new WP_REST_Response([
'success' => false,
'data' => ['message' => sprintf('Missing required field: %s', $field)]
], 400);
}
}
if (!is_numeric($params['hanger_width_mm']) || !is_numeric($params['axle_width_inches'])) {
return new WP_REST_Response([
'success' => false,
'data' => ['message' => 'Widths must be numeric values']
], 400);
}
$table_name = $wpdb->prefix . 'product_machine_truck_sizes';
$axle_width = round((float)$params['axle_width_inches'] * 8) / 8;
$result = $wpdb->update(
$table_name,
[
'brand' => sanitize_text_field($params['brand']),
'model' => sanitize_text_field($params['model']),
'hanger_width_mm' => round((float)$params['hanger_width_mm'], 2),
'axle_width_inches' => $axle_width
],
['id' => $id],
['%s', '%s', '%f', '%f'],
['%d']
);
if ($result === false) {
$message = $wpdb->last_error ?: 'Unknown database error';
if (stripos($message, 'duplicate') !== false) {
return new WP_REST_Response([
'success' => false,
'data' => ['message' => 'Truck size already exists for that brand/model']
], 409);
}
return new WP_REST_Response([
'success' => false,
'data' => ['message' => 'Failed to update truck size: ' . $message]
], 500);
}
if ($result === 0) {
$exists = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $table_name WHERE id = %d", $id));
if (!$exists) {
return new WP_REST_Response([
'success' => false,
'data' => ['message' => 'Truck size not found']
], 404);
}
}
return new WP_REST_Response([
'success' => true,
'data' => ['message' => 'Truck size updated']
], 200);
}
]);
// Delete truck size
register_rest_route('product-machine/v1', '/truck-sizes/(?P\d+)', [
'methods' => 'DELETE',
'permission_callback' => function () { return current_user_can('manage_options'); },
'callback' => function (WP_REST_Request $request) {
global $wpdb;
$id = (int) $request['id'];
$table_name = $wpdb->prefix . 'product_machine_truck_sizes';
$result = $wpdb->delete($table_name, ['id' => $id]);
if ($result === false) {
return new WP_REST_Response(['success' => false, 'data' => ['message' => 'Failed to delete truck size: ' . $wpdb->last_error]], 500);
}
if ($result === 0) {
return new WP_REST_Response(['success' => false, 'data' => ['message' => 'Truck size not found']], 404);
}
return new WP_REST_Response(['success' => true, 'data' => ['message' => 'Truck size deleted']], 200);
}
]);
// Debug endpoint to initialize truck sizes table
register_rest_route('product-machine/v1', '/truck-sizes-debug', [
'methods' => 'GET',
'permission_callback' => function () { return current_user_can('manage_options'); },
'callback' => function (WP_REST_Request $request) {
$conn = pm_get_external_db_connection();
if (!$conn) {
return new WP_REST_Response(['success' => false, 'data' => ['message' => 'Database connection failed']], 500);
}
// Check if table exists
$result = $conn->query("SHOW TABLES LIKE 'truck_sizes'");
$table_exists = $result && $result->num_rows > 0;
if (!$table_exists) {
// Create table
product_machine_create_truck_sizes_table();
$message = 'Table created and sample data populated';
} else {
// Check row count
$count_result = $conn->query("SELECT COUNT(*) as count FROM truck_sizes");
$row_count = $count_result ? $count_result->fetch_assoc()['count'] : 0;
if ($row_count == 0) {
product_machine_populate_sample_truck_sizes();
$message = 'Table exists but was empty - sample data populated';
} else {
$message = 'Table exists with ' . $row_count . ' rows';
}
}
$conn->close();
return new WP_REST_Response(['success' => true, 'data' => ['message' => $message, 'table_exists' => $table_exists]], 200);
}
]);
});
// Extra guard: dequeue problematic third-party script on our admin pages
add_action('admin_enqueue_scripts', function ($hook) {
$pm_pages = [
'toplevel_page_product-machine',
'product-machine_page_product-machine-brands',
'product-machine_page_product-machine-unpack-words',
'product-machine_page_product-machine-manage-products',
'product-machine_page_product-machine-truck-sizes'
];
if (!in_array($hook, $pm_pages)) { return; }
global $wp_scripts;
if (!$wp_scripts) { return; }
foreach ((array) $wp_scripts->queue as $handle) {
$reg = $wp_scripts->registered[$handle] ?? null;
if (!$reg) { continue; }
$src = (string) ($reg->src ?? '');
if (strpos($src, 'wf_common') !== false) {
wp_dequeue_script($handle);
wp_deregister_script($handle);
error_log('product-machine: dequeued script handle ' . $handle . ' (wf_common*.js) on ' . $hook);
}
}
}, 1000);
// Define the function if needed in `product-machine.php`
function product_machine_unpack_words_page()
{
if (!current_user_can('manage_options')) {
return;
}
include plugin_dir_path(__FILE__) . 'templates/product-machine-unpack-words.php';
}
// Function to get unpacking words from the database
function get_unpacking_words()
{
// Connect to the external database
$db = pm_get_external_db_connection();
if (!$db) {
return [];
}
$result = $db->query("SELECT * FROM product_machine_unpack_words");
if (!$result) {
$db->close();
return [];
}
$unpacking_words = [];
while ($row = $result->fetch_assoc()) {
$unpacking_words[] = $row;
}
$db->close();
return $unpacking_words;
}
add_action('init', 'register_brand_taxonomy');
function register_brand_taxonomy()
{
$attribute_slug = 'brand-name';
$attribute_label = 'Brand';
if (!taxonomy_exists($attribute_slug)) {
register_taxonomy(
$attribute_slug,
apply_filters('woocommerce_taxonomy_objects_' . $attribute_slug, ['product']),
apply_filters('woocommerce_taxonomy_args_' . $attribute_slug, [
'labels' => ['name' => $attribute_label],
'hierarchical' => true,
'show_ui' => false,
'query_var' => true,
'rewrite' => false,
])
);
}
}
// Register custom attribute taxonomies for 'wheel-diameter' and 'durometer'
add_action('init', 'register_custom_attributes_taxonomy');
function register_custom_attributes_taxonomy()
{
$attributes = [
['name' => 'wheel-diameter', 'label' => 'Wheel Diameter'],
['name' => 'durometer', 'label' => 'Durometer']
];
foreach ($attributes as $attribute) {
$attribute_taxonomy = wc_attribute_taxonomy_name($attribute['name']);
// Check if taxonomy is already registered
if (!taxonomy_exists($attribute_taxonomy)) {
register_taxonomy(
$attribute_taxonomy,
apply_filters('woocommerce_taxonomy_objects_' . $attribute_taxonomy, ['product']),
apply_filters('woocommerce_taxonomy_args_' . $attribute_taxonomy, [
'labels' => [
'name' => $attribute['label'],
],
'hierarchical' => true,
'show_ui' => false,
'query_var' => true,
'rewrite' => false,
])
);
}
}
}
// Function to get pebs_cat_margin from the database
function get_pebs_cat_margin($pebs_cat_code)
{
static $margin_cache = [];
if (array_key_exists($pebs_cat_code, $margin_cache)) {
return $margin_cache[$pebs_cat_code];
}
// First, try to read margin from the in-plugin Category Reference option
$cats = get_option('product_machine_category_ref', []);
if (is_array($cats) && !empty($cats)) {
foreach ($cats as $c) {
if (isset($c['code']) && strtoupper(trim($c['code'])) === strtoupper(trim($pebs_cat_code))) {
// Expect 'margin' as percent (e.g., 50 = 50%). Convert to multiplier.
$m = isset($c['margin']) ? floatval($c['margin']) : null;
if ($m !== null && $m !== '') {
$mult = 1.0 + ($m / 100.0);
$margin_cache[$pebs_cat_code] = $mult;
return $margin_cache[$pebs_cat_code];
}
}
}
}
// Fallback: connect to the external database (legacy source)
$db = pm_get_external_db_connection();
if (!$db) {
$margin_cache[$pebs_cat_code] = false;
return false;
}
// Prepare and execute the query
$stmt = $db->prepare("SELECT pebs_cat_margin FROM product_machine WHERE pebs_cat_code = ?");
$stmt->bind_param('s', $pebs_cat_code);
$stmt->execute();
$stmt->bind_result($pebs_cat_margin);
$stmt->fetch();
$stmt->close();
$db->close();
// Normalize legacy DB value: if it's a reasonable percent (1..100) treat as percent, if it's a multiplier (<1 or >1 with decimals) use as-is
if ($pebs_cat_margin === null || $pebs_cat_margin === '') {
$margin_cache[$pebs_cat_code] = false;
return false;
}
$val = floatval($pebs_cat_margin);
if ($val > 0 && $val <= 100 && intval($val) == $val) {
// integer percent like 50 -> 1.5
$mult = 1.0 + ($val / 100.0);
} elseif ($val > 0 && $val < 1.0) {
// already a multiplier like 0.5 (unlikely) — treat as multiplier
$mult = $val;
} elseif ($val > 1.0 && $val < 10.0) {
// could be multiplier like 1.5 or percent expressed as 150 (unlikely); assume multiplier
$mult = $val;
} else {
// Fallback: treat as false
$margin_cache[$pebs_cat_code] = false;
return false;
}
$margin_cache[$pebs_cat_code] = $mult;
return $margin_cache[$pebs_cat_code];
}
function map_raw_data_to_wc_csv_fields($product_data)
{
return [
'ID' => '',
'Type' => 'simple',
'SKU' => $product_data['SKU'] ?? '',
'Name' => $product_data['Name'] ?? '',
'Published' => $product_data['Published'] ?? '1',
'Is featured?' => $product_data['Is_featured'] ?? '0',
'Visibility in catalog' => $product_data['Visibility_in_catalog'] ?? 'visible',
'Short description' => $product_data['Short_description'] ?? '',
'Description' => $product_data['Description'] ?? '',
'Tax status' => $product_data['Tax_status'] ?? 'taxable',
'In stock?' => '1',
'Stock' => $product_data['Stock'] ?? '0',
'Regular price' => $product_data['Regular_price'] ?? '',
'Categories' => $product_data['Categories'] ?? '',
'Weight (lbs)' => $product_data['Weight_lbs'] ?? '',
'Width (in)' => $product_data['Width_in'] ?? '',
// Add other fields as required by WooCommerce...
];
}
// add_action('admin_enqueue_scripts', 'product_machine_admin_scripts');
function pm_test_db_connection()
{
$db = pm_get_external_db_connection();
if (!$db) {
echo 'Database connection error: ' . mysqli_connect_error();
} else {
echo 'Database connection successful!';
$db->close();
}
die(); // Stop further execution
}
function product_machine_import_data()
{
if (empty($_POST['raw-data'])) {
echo 'Please provide the raw data.
';
return;
}
$raw_data = sanitize_textarea_field($_POST['raw-data']);
$invoice_name = isset($_POST['invoicename']) ? sanitize_text_field($_POST['invoicename']) : '';
$outputstyle = isset($_POST['outputstyle']) ? sanitize_text_field($_POST['outputstyle']) : 'screen';
$sku_usage = isset($_POST['sku_usage']) ? sanitize_text_field($_POST['sku_usage']) : 'default';
$products = product_machine_process_raw_data($raw_data, $invoice_name, $sku_usage);
if ($outputstyle == 'csv') {
product_machine_export_csv($products);
} else {
$errors = [];
$success_count = 0;
}
foreach ($products as $product_data) {
// Create or update product in WooCommerce
// function create_or_update_product($quantity, $name, $price, $product_data, $sku)
$result = create_or_update_product(
$product_data['Stock'],
$product_data['Name'],
$product_data['Regular_price'],
$product_data,
$product_data['SKU']
);
// create_or_update_product returns product ID (int); count as success
$success_count++;
}
if (!empty($pm_errors)) {
echo 'Warnings:
' . implode('
', array_map('esc_html', $pm_errors)) . '
';
}
// Display results
if ($success_count > 0) {
echo 'Successfully imported ' . $success_count . ' products.
';
}
if (!empty($errors)) {
echo '' . implode('
', $errors) . '
';
}
}
// Main plugin page
function product_machine_page()
{
if (!current_user_can('manage_options')) {
return;
}
// Auto-complete the invoice/invoice_line_number TODO once, if it's still open.
try {
$todos = get_option('product_machine_todos', []);
if (is_array($todos)) {
$changed = false;
$has_cost_row = false;
foreach ($todos as $i => $t) {
$desc = isset($t['description']) ? strtolower($t['description']) : '';
$status = isset($t['status']) ? $t['status'] : 'not-started';
if ($status !== 'completed' && strpos($desc, 'invoice') !== false && strpos($desc, 'invoice_line') !== false) {
$todos[$i]['status'] = 'completed';
// Prefer the code commit that fixed the write path
$todos[$i]['commit'] = 'ed3e5d09';
if (!isset($todos[$i]['completed_at']) || empty($todos[$i]['completed_at'])) {
$todos[$i]['completed_at'] = date('c');
}
$changed = true;
}
// Also find/auto-complete the cost fields row if present
if (strpos($desc, 'cost') !== false && (strpos($desc, 'cog') !== false || strpos($desc, 'alg_wc_cog_cost') !== false)) {
$has_cost_row = true;
if ($status !== 'completed') {
$todos[$i]['status'] = 'completed';
$todos[$i]['commit'] = 'b906964e'; // commit that writes both fields + preview check
if (!isset($todos[$i]['completed_at']) || empty($todos[$i]['completed_at'])) {
$todos[$i]['completed_at'] = date('c');
}
$changed = true;
}
}
}
// Ensure an Attributes fact-checker row exists and is completed
$has_attr_row = false;
foreach ($todos as $i => $t) {
$desc = isset($t['description']) ? strtolower($t['description']) : '';
if (strpos($desc, 'attribute') !== false && (strpos($desc, 'fact-check') !== false || strpos($desc, 'checker') !== false)) {
$has_attr_row = true;
if ($todos[$i]['status'] !== 'completed') {
$todos[$i]['status'] = 'completed';
// Reserve 'commit' for real hashes; record timestamp in 'completed_at'
if (empty($todos[$i]['completed_at'])) { $todos[$i]['completed_at'] = date('c'); }
if (!isset($todos[$i]['commit']) || !preg_match('/^[0-9a-f]{7,40}$/i', (string) $todos[$i]['commit'])) {
$todos[$i]['commit'] = '';
}
$changed = true;
}
}
}
if (!$has_attr_row) {
$todos[] = [
'description' => "Attributes fact-checker after 'Update' (Brand/Width/Size/Durometer; immediate UI flip)",
'status' => 'completed',
'commit' => '', // no specific commit hash recorded
'completed_at' => date('c'),
];
$changed = true;
}
// Ensure a Brand Add URL/schema fix row exists and is completed
$has_brand_row = false;
foreach ($todos as $i => $t) {
$desc = isset($t['description']) ? strtolower($t['description']) : '';
if (strpos($desc, 'brand') !== false && (strpos($desc, 'url') !== false || strpos($desc, 'eastern_brand_code') !== false)) {
$has_brand_row = true;
if ($todos[$i]['status'] !== 'completed') {
$todos[$i]['status'] = 'completed';
if (empty($todos[$i]['completed_at'])) { $todos[$i]['completed_at'] = date('c'); }
if (!isset($todos[$i]['commit']) || !preg_match('/^[0-9a-f]{7,40}$/i', (string) $todos[$i]['commit'])) {
$todos[$i]['commit'] = '';
}
$changed = true;
}
}
}
if (!$has_brand_row) {
$todos[] = [
'description' => "Brand Add: strip http(s) from URL and include eastern_brand_code",
'status' => 'completed',
'commit' => '',
'completed_at' => date('c'),
];
$changed = true;
}
// If there is no cost QA row at all, append one with explicit wording
if (!$has_cost_row) {
$todos[] = [
'description' => "Both costs QA'd and updated w/ 'Update' (ACF cost + _alg_wc_cog_cost)",
'status' => 'completed',
'commit' => 'b906964e',
'completed_at' => date('c'),
];
$changed = true;
}
// Remove duplication: 'Bones Bearings Tool' TODO is considered part of 'Bones brand verification'
if ($changed) {
update_option('product_machine_todos', $todos);
}
// Deduplicate TODOs by normalized description, preferring entries with a commit
$normalized = [];
$deduped = [];
foreach ($todos as $t) {
$key = strtolower(trim(preg_replace('/\s+/', ' ', ($t['description'] ?? ''))));
if ($key === '') continue;
if (!isset($normalized[$key])) {
$normalized[$key] = $t;
} else {
// If the existing entry doesn't have a commit but new one does, replace
$existing = $normalized[$key];
$existing_commit = isset($existing['commit']) ? $existing['commit'] : '';
$new_commit = isset($t['commit']) ? $t['commit'] : '';
if (empty($existing_commit) && !empty($new_commit)) {
$normalized[$key] = $t;
}
// else keep existing
}
}
// Turn back into numeric-indexed array
foreach ($normalized as $k => $v) { $deduped[] = $v; }
if (count($deduped) !== count($todos)) {
$todos = $deduped;
update_option('product_machine_todos', $todos);
}
// Also auto-complete several TODOs with known commit hashes (6-12)
$auto_complete_map = [
'add sticker' => '4fb59ef6', // #6: Add 'sticker' keyword
'sticker keyword' => '4fb59ef6',
'sticky header' => 'c6ef1f6c', // #7-8 sticky header fix (merged redundant #7/#8)
'sticky headers' => 'c6ef1f6c', // accept plural phrasing
'sticky header fix' => 'c6ef1f6c', // alternate phrasing
'20pk' => '9e081bdc', // #9 migration for 20pk
'prioritize sticker' => '6628313c', // #10: sticker category priority
'tools category code' => '02dc21f7', // #11: change TL to TO
'bones brand processing' => '76cc5d3d', // #12: bones verification script
];
$ac_changed = false;
foreach ($auto_complete_map as $needle => $commit_hash) {
$found = false;
foreach ($todos as $i => $t) {
$desc = isset($t['description']) ? strtolower($t['description']) : '';
if (stripos($desc, $needle) !== false) {
$found = true;
if (!isset($todos[$i]['status']) || $todos[$i]['status'] !== 'completed') {
$todos[$i]['status'] = 'completed';
$todos[$i]['commit'] = $commit_hash;
if (!isset($todos[$i]['completed_at']) || empty($todos[$i]['completed_at'])) {
$todos[$i]['completed_at'] = date('c');
}
$ac_changed = true;
} else {
// ensure commit is filled
if (empty($todos[$i]['commit'])) {
$todos[$i]['commit'] = $commit_hash;
$ac_changed = true;
}
}
break;
}
}
if (!$found) {
// append as completed if not found
$todos[] = [
'description' => ucfirst($needle),
'status' => 'completed',
'commit' => $commit_hash,
'completed_at' => date('c'),
];
$ac_changed = true;
}
}
if ($ac_changed) {
update_option('product_machine_todos', $todos);
}
// Remove redundant 'Bones Bearings Tool' todo entries (duplicate of Bones verification)
$removed_any = false;
foreach ($todos as $idx => $t) {
if (!empty($t['description']) && stripos($t['description'], 'Bones Bearings Tool') !== false) {
unset($todos[$idx]);
$removed_any = true;
}
}
if ($removed_any) {
// reindex array
$todos = array_values($todos);
update_option('product_machine_todos', $todos);
}
}
} catch (Throwable $t) {
// Non-fatal: keep page loading if option is unavailable
error_log('PM TODO auto-complete skipped: ' . $t->getMessage());
}
// Process form submission
if (isset($_POST['submit']) && $_POST['submit'] == "Import Products") {
check_admin_referer('product_machine_nonce_action', 'product_machine_nonce_field');
product_machine_import_data();
}
// Display the form
?>
Product Machine