xonack

wp-woocommerce

0
0
# Install this skill:
npx skills add xonack/wp-woocommerce-claude-skill --skill "wp-woocommerce"

Install specific skill from multi-skill repository

# Description

WooCommerce development patterns β€” custom product types, payment gateways, shipping methods, HPOS, REST API extensions, cart/checkout customization, email classes, block integration, and testing with WC helpers.

# SKILL.md


name: wp-woocommerce
description: WooCommerce development patterns β€” custom product types, payment gateways, shipping methods, HPOS, REST API extensions, cart/checkout customization, email classes, block integration, and testing with WC helpers.
tools:
- read
- write
- edit
- bash
- grep
- glob


WooCommerce Development

1. WooCommerce Architecture

WooCommerce is a singleton plugin built on WordPress. The WC() function returns the global instance. Key architectural layers:

  • WC_Data β€” Abstract CRUD base class. All major entities (products, orders, coupons) extend this.
  • Data Stores β€” Abstraction layer between CRUD classes and the database. Swappable via woocommerce_data_stores filter.
  • Session Handler β€” WC_Session_Handler stores cart/session data. Uses a custom table (wc_sessions), not PHP sessions.
  • WC_Install β€” Bootstrap class that runs on activation. Creates tables, sets default options, schedules cron events.
// Access WooCommerce singleton
$wc = WC();
$cart = WC()->cart;
$session = WC()->session;

// Override a data store
add_filter( 'woocommerce_data_stores', function( $stores ) {
    $stores['product'] = 'My_Custom_Product_Data_Store';
    return $stores;
});

2. Custom Product Types

Extend WC_Product and register the type with WooCommerce admin.

// Step 1: Define the product class
class WC_Product_Bundle extends WC_Product {
    public function get_type() {
        return 'bundle';
    }

    public function get_bundled_items() {
        return $this->get_meta( '_bundled_product_ids', true ) ?: [];
    }
}

// Step 2: Register the product type class
add_filter( 'woocommerce_product_class', function( $classname, $product_type ) {
    if ( $product_type === 'bundle' ) {
        return 'WC_Product_Bundle';
    }
    return $classname;
}, 10, 2 );

// Step 3: Add to product type selector in admin
add_filter( 'product_type_selector', function( $types ) {
    $types['bundle'] = __( 'Bundle Product', 'my-plugin' );
    return $types;
});

// Step 4: Add product data tab
add_filter( 'woocommerce_product_data_tabs', function( $tabs ) {
    $tabs['bundle_options'] = [
        'label'    => __( 'Bundle Items', 'my-plugin' ),
        'target'   => 'bundle_product_data',
        'class'    => [ 'show_if_bundle' ],
        'priority' => 25,
    ];
    return $tabs;
});

// Step 5: Render the tab panel
add_action( 'woocommerce_product_data_panels', function() {
    echo '<div id="bundle_product_data" class="panel woocommerce_options_panel">';
    woocommerce_wp_text_input([
        'id'          => '_bundled_product_ids',
        'label'       => __( 'Bundled Product IDs', 'my-plugin' ),
        'description' => __( 'Comma-separated product IDs', 'my-plugin' ),
        'desc_tip'    => true,
    ]);
    echo '</div>';
});

// Step 6: Save product data
add_action( 'woocommerce_process_product_meta_bundle', function( $post_id ) {
    $product = wc_get_product( $post_id );
    $ids = sanitize_text_field( $_POST['_bundled_product_ids'] ?? '' );
    $product->update_meta_data( '_bundled_product_ids', array_map( 'absint', explode( ',', $ids ) ) );
    $product->save();
});

3. Payment Gateway Development

Extend WC_Payment_Gateway. The process_payment method is the core contract.

class WC_Gateway_Custom extends WC_Payment_Gateway {
    public function __construct() {
        $this->id                 = 'custom_gateway';
        $this->method_title       = __( 'Custom Gateway', 'my-plugin' );
        $this->method_description = __( 'Accept payments via Custom API.', 'my-plugin' );
        $this->has_fields         = true;
        $this->supports           = [ 'products', 'refunds', 'tokenization' ];

        $this->init_form_fields();
        $this->init_settings();
        $this->title   = $this->get_option( 'title' );
        $this->testmode = 'yes' === $this->get_option( 'testmode' );

        add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, [ $this, 'process_admin_options' ] );
        add_action( 'woocommerce_api_' . strtolower( get_class( $this ) ), [ $this, 'handle_webhook' ] );
    }

    public function init_form_fields() {
        $this->form_fields = [
            'enabled'  => [ 'title' => 'Enable', 'type' => 'checkbox', 'default' => 'no' ],
            'title'    => [ 'title' => 'Title', 'type' => 'text', 'default' => 'Custom Pay' ],
            'testmode' => [ 'title' => 'Test Mode', 'type' => 'checkbox', 'default' => 'yes' ],
            'api_key'  => [ 'title' => 'API Key', 'type' => 'password' ],
        ];
    }

    public function process_payment( $order_id ) {
        $order = wc_get_order( $order_id );

        $response = wp_remote_post( $this->get_api_url() . '/charges', [
            'headers' => [ 'Authorization' => 'Bearer ' . $this->get_option( 'api_key' ) ],
            'body'    => wp_json_encode([
                'amount'   => $order->get_total() * 100,
                'currency' => $order->get_currency(),
                'metadata' => [ 'order_id' => $order_id ],
            ]),
        ]);

        $body = json_decode( wp_remote_retrieve_body( $response ), true );

        if ( ! empty( $body['id'] ) && $body['status'] === 'succeeded' ) {
            $order->payment_complete( $body['id'] );
            $order->add_order_note( sprintf( 'Payment completed. Transaction ID: %s', $body['id'] ) );
            WC()->cart->empty_cart();
            return [ 'result' => 'success', 'redirect' => $this->get_return_url( $order ) ];
        }

        wc_add_notice( $body['error']['message'] ?? 'Payment failed.', 'error' );
        return [ 'result' => 'failure' ];
    }

    public function process_refund( $order_id, $amount = null, $reason = '' ) {
        $order = wc_get_order( $order_id );
        $txn_id = $order->get_transaction_id();
        if ( ! $txn_id ) return new WP_Error( 'no_txn', 'No transaction ID found.' );

        $response = wp_remote_post( $this->get_api_url() . '/refunds', [
            'headers' => [ 'Authorization' => 'Bearer ' . $this->get_option( 'api_key' ) ],
            'body'    => wp_json_encode([ 'charge' => $txn_id, 'amount' => $amount * 100 ]),
        ]);

        $body = json_decode( wp_remote_retrieve_body( $response ), true );
        return ! empty( $body['id'] ) ? true : new WP_Error( 'refund_failed', 'Refund failed.' );
    }

    public function handle_webhook() {
        $payload = json_decode( file_get_contents( 'php://input' ), true );
        $order   = wc_get_order( $payload['metadata']['order_id'] ?? 0 );
        if ( $order && $payload['type'] === 'charge.refunded' ) {
            $order->update_status( 'refunded', 'Webhook: refund confirmed.' );
        }
        wp_die( 'OK', 200 );
    }

    private function get_api_url() {
        return $this->testmode ? 'https://api.sandbox.example.com' : 'https://api.example.com';
    }
}

add_filter( 'woocommerce_payment_gateways', function( $gateways ) {
    $gateways[] = 'WC_Gateway_Custom';
    return $gateways;
});

4. Shipping Method Development

class WC_Shipping_Custom extends WC_Shipping_Method {
    public function __construct( $instance_id = 0 ) {
        $this->id                 = 'custom_shipping';
        $this->instance_id        = absint( $instance_id );
        $this->method_title       = __( 'Custom Shipping', 'my-plugin' );
        $this->method_description = __( 'Real-time rate calculation.', 'my-plugin' );
        $this->supports           = [ 'shipping-zones', 'instance-settings' ];

        $this->init();
    }

    public function init() {
        $this->instance_form_fields = [
            'base_cost' => [
                'title'   => __( 'Base Cost', 'my-plugin' ),
                'type'    => 'price',
                'default' => '5.00',
            ],
            'per_kg' => [
                'title'   => __( 'Cost per kg', 'my-plugin' ),
                'type'    => 'price',
                'default' => '1.50',
            ],
        ];
        $this->title = $this->get_option( 'title', $this->method_title );
    }

    public function calculate_shipping( $package = [] ) {
        $weight    = 0;
        $base_cost = (float) $this->get_option( 'base_cost', 5 );
        $per_kg    = (float) $this->get_option( 'per_kg', 1.5 );

        foreach ( $package['contents'] as $item ) {
            $product = $item['data'];
            $weight += (float) $product->get_weight() * $item['quantity'];
        }

        $cost = $base_cost + ( $weight * $per_kg );

        $this->add_rate([
            'id'    => $this->get_rate_id(),
            'label' => $this->title,
            'cost'  => $cost,
        ]);
    }
}

add_filter( 'woocommerce_shipping_methods', function( $methods ) {
    $methods['custom_shipping'] = 'WC_Shipping_Custom';
    return $methods;
});

5. WooCommerce Hooks Reference

Critical Action Hooks

// Validate checkout before processing
add_action( 'woocommerce_checkout_process', function() {
    if ( empty( $_POST['custom_field'] ) ) {
        wc_add_notice( __( 'Custom field is required.' ), 'error' );
    }
});

// After payment completes (order paid)
add_action( 'woocommerce_payment_complete', function( $order_id ) {
    $order = wc_get_order( $order_id );
    // Trigger fulfillment, send to ERP, etc.
}, 10, 1 );

// Order status transitions
add_action( 'woocommerce_order_status_changed', function( $order_id, $from, $to, $order ) {
    if ( $to === 'completed' ) {
        // Grant access, generate license keys, etc.
    }
}, 10, 4 );

// Item added to cart
add_action( 'woocommerce_add_to_cart', function( $cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data ) {
    // Analytics tracking, cross-sell logic, etc.
}, 10, 6 );

// New order created (fires before payment)
add_action( 'woocommerce_new_order', function( $order_id, $order ) {
    // Reserve inventory, fraud check, etc.
}, 10, 2 );

Critical Filter Hooks

// Modify displayed cart item price
add_filter( 'woocommerce_cart_item_price', function( $price, $cart_item, $cart_item_key ) {
    return $price . ' <small>(per unit)</small>';
}, 10, 3 );

// Modify available shipping rates
add_filter( 'woocommerce_package_rates', function( $rates, $package ) {
    // Hide free shipping if other methods exist
    $dominated = [];
    foreach ( $rates as $rate_id => $rate ) {
        if ( 'free_shipping' === $rate->method_id && count( $rates ) > 1 ) {
            $dominated[] = $rate_id;
        }
    }
    return array_diff_key( $rates, array_flip( $dominated ) );
}, 10, 2 );

// Customize checkout fields
add_filter( 'woocommerce_checkout_fields', function( $fields ) {
    $fields['billing']['billing_vat'] = [
        'type'     => 'text',
        'label'    => __( 'VAT Number', 'my-plugin' ),
        'required' => false,
        'priority' => 120,
    ];
    unset( $fields['billing']['billing_company'] );
    return $fields;
});

// Control "add to cart" validation
add_filter( 'woocommerce_add_to_cart_validation', function( $passed, $product_id, $quantity ) {
    $product = wc_get_product( $product_id );
    if ( $product->get_meta( '_requires_approval' ) && ! current_user_can( 'approved_buyer' ) ) {
        wc_add_notice( __( 'This product requires buyer approval.' ), 'error' );
        return false;
    }
    return $passed;
}, 10, 3 );

6. REST API Extensions

add_action( 'rest_api_init', function() {
    register_rest_route( 'my-plugin/v1', '/inventory/(?P<id>\d+)', [
        'methods'             => 'GET',
        'callback'            => 'mp_get_inventory',
        'permission_callback' => function( $request ) {
            return current_user_can( 'manage_woocommerce' );
        },
        'args' => [
            'id' => [ 'validate_callback' => 'is_numeric', 'required' => true ],
        ],
    ]);
});

function mp_get_inventory( WP_REST_Request $request ) {
    $product = wc_get_product( $request['id'] );
    if ( ! $product ) {
        return new WP_Error( 'not_found', 'Product not found', [ 'status' => 404 ] );
    }
    return rest_ensure_response([
        'product_id'    => $product->get_id(),
        'sku'           => $product->get_sku(),
        'stock_status'  => $product->get_stock_status(),
        'stock_quantity' => $product->get_stock_quantity(),
        'manage_stock'  => $product->get_manage_stock(),
    ]);
}

Extend the WC REST API namespace for deeper integration:

// Register a custom batch endpoint compatible with WC API conventions
add_filter( 'woocommerce_rest_api_get_rest_namespaces', function( $controllers ) {
    $controllers['wc/v3']['custom-inventory'] = 'WC_REST_Custom_Inventory_Controller';
    return $controllers;
});

7. HPOS (High-Performance Order Storage)

WooCommerce 8.2+ uses custom order tables (wc_orders, wc_order_addresses, wc_order_operational_data) instead of wp_posts and wp_postmeta. Declare compatibility:

add_action( 'before_woocommerce_init', function() {
    if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) {
        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
            'custom_order_tables',
            __FILE__,
            true
        );
    }
});

Use OrderUtil for HPOS-safe queries:

use Automattic\WooCommerce\Utilities\OrderUtil;

// Check if HPOS is active
if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
    // Use wc_get_orders() β€” works with both storage backends
    $orders = wc_get_orders([
        'status'     => 'processing',
        'limit'      => 50,
        'meta_query' => [
            [ 'key' => '_custom_flag', 'value' => '1' ],
        ],
    ]);
} else {
    // Legacy post-based query (fallback)
    $orders = wc_get_orders([
        'status' => 'processing',
        'limit'  => 50,
    ]);
}

// HPOS-safe order screen detection in admin
add_action( 'current_screen', function( $screen ) {
    if ( OrderUtil::is_order_list_table_screen() ) {
        // We are on the orders list screen
    }
    if ( OrderUtil::is_order_edit_screen() ) {
        // We are on the single order edit screen
    }
});

// Custom meta box that works with HPOS
add_action( 'add_meta_boxes', function() {
    $screen = OrderUtil::custom_orders_table_usage_is_enabled()
        ? wc_get_page_screen_id( 'shop-order' )
        : 'shop_order';

    add_meta_box( 'my_custom_box', 'Custom Data', 'render_my_custom_box', $screen, 'side' );
});

8. Cart and Checkout Customization

// Add a custom checkout field, validate it, save it
add_action( 'woocommerce_after_order_notes', function( $checkout ) {
    woocommerce_form_field( 'delivery_date', [
        'type'     => 'date',
        'label'    => __( 'Preferred Delivery Date', 'my-plugin' ),
        'required' => true,
    ], $checkout->get_value( 'delivery_date' ) );
});

add_action( 'woocommerce_checkout_process', function() {
    if ( empty( $_POST['delivery_date'] ) ) {
        wc_add_notice( __( 'Please select a delivery date.' ), 'error' );
    }
    $date = sanitize_text_field( $_POST['delivery_date'] );
    if ( strtotime( $date ) < strtotime( '+2 days' ) ) {
        wc_add_notice( __( 'Delivery date must be at least 2 days from now.' ), 'error' );
    }
});

add_action( 'woocommerce_checkout_update_order_meta', function( $order_id ) {
    if ( ! empty( $_POST['delivery_date'] ) ) {
        $order = wc_get_order( $order_id );
        $order->update_meta_data( '_delivery_date', sanitize_text_field( $_POST['delivery_date'] ) );
        $order->save();
    }
});

// Add a dynamic cart fee
add_action( 'woocommerce_cart_calculate_fees', function( $cart ) {
    if ( is_admin() && ! defined( 'DOING_AJAX' ) ) return;

    $total = $cart->get_subtotal();
    if ( $total > 200 ) {
        $cart->add_fee( __( 'Insurance', 'my-plugin' ), 4.99, true );
    }
});

9. Custom Email Classes

class WC_Email_Custom_Shipped extends WC_Email {
    public function __construct() {
        $this->id             = 'custom_shipped';
        $this->title          = __( 'Order Shipped', 'my-plugin' );
        $this->description    = __( 'Sent when an order is marked as shipped.', 'my-plugin' );
        $this->heading        = __( 'Your order has shipped!', 'my-plugin' );
        $this->subject        = __( 'Your {site_title} order #{order_number} has shipped', 'my-plugin' );
        $this->template_html  = 'emails/custom-shipped.php';
        $this->template_plain = 'emails/plain/custom-shipped.php';
        $this->template_base  = plugin_dir_path( __DIR__ ) . 'templates/';
        $this->customer_email = true;

        // Trigger on custom status transition
        add_action( 'woocommerce_order_status_processing_to_shipped_notification', [ $this, 'trigger' ], 10, 2 );

        parent::__construct();
    }

    public function trigger( $order_id, $order = false ) {
        if ( $order_id && ! is_a( $order, 'WC_Order' ) ) {
            $order = wc_get_order( $order_id );
        }
        if ( ! $order ) return;

        $this->object    = $order;
        $this->recipient = $order->get_billing_email();
        $this->placeholders['{order_number}'] = $order->get_order_number();
        $this->placeholders['{tracking_url}'] = $order->get_meta( '_tracking_url' );

        if ( $this->is_enabled() && $this->get_recipient() ) {
            $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() );
        }
    }

    public function get_content_html() {
        return wc_get_template_html( $this->template_html, [
            'order'         => $this->object,
            'email_heading' => $this->get_heading(),
            'tracking_url'  => $this->object->get_meta( '_tracking_url' ),
            'sent_to_admin' => false,
            'plain_text'    => false,
            'email'         => $this,
        ], '', $this->template_base );
    }
}

add_filter( 'woocommerce_email_classes', function( $emails ) {
    $emails['WC_Email_Custom_Shipped'] = new WC_Email_Custom_Shipped();
    return $emails;
});

10. WooCommerce Blocks Integration

The block-based checkout uses the Store API. Extend it with custom data and blocks.

// Register endpoint data for the Store API (checkout block)
use Automattic\WooCommerce\StoreApi\Schemas\V1\CheckoutSchema;
use Automattic\WooCommerce\StoreApi\StoreApi;

add_action( 'woocommerce_blocks_loaded', function() {
    woocommerce_store_api_register_endpoint_data([
        'endpoint'        => CheckoutSchema::IDENTIFIER,
        'namespace'       => 'my-plugin',
        'data_callback'   => function() {
            return [ 'delivery_date' => '' ];
        },
        'schema_callback' => function() {
            return [
                'delivery_date' => [
                    'description' => 'Preferred delivery date',
                    'type'        => 'string',
                    'context'     => [ 'view', 'edit' ],
                ],
            ];
        },
    ]);

    // Validate and save the block checkout extension data
    woocommerce_store_api_register_update_callback([
        'namespace' => 'my-plugin',
        'callback'  => function( $data ) {
            if ( empty( $data['delivery_date'] ) ) {
                throw new \Exception( 'Delivery date is required.' );
            }
        },
    ]);
});

// Save the extension data to order meta after checkout
add_action( 'woocommerce_store_api_checkout_update_order_from_request', function( $order, $request ) {
    $extensions = $request['extensions']['my-plugin'] ?? [];
    if ( ! empty( $extensions['delivery_date'] ) ) {
        $order->update_meta_data( '_delivery_date', sanitize_text_field( $extensions['delivery_date'] ) );
    }
}, 10, 2 );

Register the JS block:

// checkout-block/index.js
import { registerCheckoutBlock } from '@woocommerce/blocks-checkout';
import { DeliveryDateBlock } from './delivery-date-block';

registerCheckoutBlock({
    metadata: {
        name: 'my-plugin/delivery-date',
        parent: [ 'woocommerce/checkout-shipping-methods-block' ],
    },
    component: DeliveryDateBlock,
});

11. Testing WooCommerce

Use WC_Unit_Test_Case as the base class. WooCommerce provides helper factories.

class Test_Custom_Product extends WC_Unit_Test_Case {
    public function test_bundle_product_type() {
        $product = new WC_Product_Bundle();
        $this->assertEquals( 'bundle', $product->get_type() );
    }

    public function test_order_payment_complete_fires_hook() {
        $order = WC_Helper_Order::create_order();
        $order->set_payment_method( 'custom_gateway' );
        $order->save();

        $fired = false;
        add_action( 'woocommerce_payment_complete', function() use ( &$fired ) {
            $fired = true;
        });

        $order->payment_complete( 'txn_test_123' );

        $this->assertTrue( $fired );
        $this->assertEquals( 'completed', $order->get_status() );
        $this->assertEquals( 'txn_test_123', $order->get_transaction_id() );
    }

    public function test_shipping_rate_calculation() {
        $method  = new WC_Shipping_Custom();
        $product = WC_Helper_Product::create_simple_product( true, [ 'weight' => '2' ] );

        $package = [
            'contents' => [
                [ 'data' => $product, 'quantity' => 3 ],
            ],
            'destination' => [
                'country'  => 'US',
                'state'    => 'CA',
                'postcode' => '90210',
            ],
        ];

        $method->calculate_shipping( $package );
        $rates = $method->rates;

        $this->assertCount( 1, $rates );
        // base_cost (5) + weight (2 * 3 = 6) * per_kg (1.5) = 5 + 9 = 14
        $this->assertEquals( 14, $rates[ array_key_first( $rates ) ]->cost );
    }

    public function test_custom_checkout_field_validation() {
        // Simulate missing delivery_date
        $_POST['delivery_date'] = '';

        do_action( 'woocommerce_checkout_process' );

        $notices = wc_get_notices( 'error' );
        $this->assertNotEmpty( $notices );

        wc_clear_notices();
    }

    public function test_rest_api_inventory_endpoint() {
        $product = WC_Helper_Product::create_simple_product( true, [
            'sku'            => 'TEST-SKU-001',
            'stock_quantity' => 42,
            'manage_stock'   => true,
        ] );

        wp_set_current_user( $this->factory->user->create( [ 'role' => 'administrator' ] ) );

        $request  = new WP_REST_Request( 'GET', '/my-plugin/v1/inventory/' . $product->get_id() );
        $response = rest_do_request( $request );
        $data     = $response->get_data();

        $this->assertEquals( 200, $response->get_status() );
        $this->assertEquals( 'TEST-SKU-001', $data['sku'] );
        $this->assertEquals( 42, $data['stock_quantity'] );
    }

    public function test_hpos_compatibility_declared() {
        $this->assertTrue(
            class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ),
            'FeaturesUtil class must exist for HPOS compatibility declaration.'
        );
    }
}

Run tests with wp-env:

# Start the WP test environment
wp-env start

# Run WooCommerce tests
wp-env run tests-cli ./vendor/bin/phpunit --filter=Test_Custom_Product

# Run E2E tests (Playwright)
cd wp-content/plugins/my-plugin && npx playwright test

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.