From atum-cms-ecom
WooCommerce 8+ pattern library for WordPress e-commerce — custom product types (WC_Product extensions), cart and checkout hooks (woocommerce_before_cart, woocommerce_checkout_fields, woocommerce_checkout_process), custom shipping methods (WC_Shipping_Method subclasses), custom payment gateways (WC_Payment_Gateway with PSD2/SCA compliance), REST API v3 endpoints and Store API v1 (headless-ready), webhooks, metadata and order meta, High-Performance Order Storage (HPOS) compatibility declaration (Automattic WooCommerce FeaturesUtil::declare_compatibility), and integration with block themes via WooCommerce Blocks. Use whenever building or maintaining a WooCommerce store: custom products, checkout extensions, payment gateways, shipping methods, REST API consumers, HPOS migration, or Gutenberg product block customization. Differentiates from wordpress-patterns by covering only the commerce-specific layer of WordPress — products, carts, orders, payments, shipping. Companion to wordpress-expert agent for WooCommerce-heavy projects.
npx claudepluginhub arnwaldn/atum-plugins-collection --plugin atum-cms-ecomThis skill uses the workspace's default tool permissions.
Patterns WooCommerce pour projets WordPress e-commerce. **À lire avec `wordpress-patterns`** (même plugin) qui couvre la couche WordPress de base.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Patterns WooCommerce pour projets WordPress e-commerce. À lire avec wordpress-patterns (même plugin) qui couvre la couche WordPress de base.
Version minimum : WooCommerce 8.2+ (HPOS stable). Tout code antérieur suppose l'ancien stockage post-based et doit être migré.
HPOS (High-Performance Order Storage) est le nouveau stockage des orders dans des tables dédiées (wp_wc_orders, wp_wc_order_addresses, etc.) au lieu de wp_posts + wp_postmeta. Tout plugin tiers doit déclarer sa compatibilité sinon WooCommerce refuse l'activation de HPOS.
add_action('before_woocommerce_init', function () {
if (class_exists('\Automattic\WooCommerce\Utilities\FeaturesUtil')) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables',
__FILE__,
true
);
}
});
À ajouter dans le fichier principal du plugin, avant toute autre logique qui touche les orders.
Comprendre l'ordre dans lequel les actions/filters se déclenchent est crucial :
Utilisateur ajoute au cart
→ woocommerce_add_to_cart_validation
→ woocommerce_add_to_cart (après ajout réussi)
Utilisateur va au cart
→ woocommerce_before_cart
→ woocommerce_cart_contents
Utilisateur va au checkout
→ woocommerce_before_checkout_form
→ woocommerce_checkout_fields (filter pour modifier les champs)
→ woocommerce_checkout_before_customer_details
→ woocommerce_review_order_before_payment
Utilisateur soumet la commande
→ woocommerce_checkout_process (validation)
→ woocommerce_checkout_create_order (avant sauvegarde)
→ woocommerce_checkout_order_processed (après sauvegarde)
→ woocommerce_new_order
Paiement traité
→ woocommerce_payment_complete
→ woocommerce_order_status_{from}_to_{to}
→ woocommerce_order_status_changed
→ woocommerce_order_status_processing (statut final typique)
$product = wc_get_product($product_id);
if (!$product instanceof WC_Product) {
return;
}
echo $product->get_name();
echo $product->get_price_html();
echo $product->is_in_stock() ? 'In stock' : 'Out of stock';
// 1. Enregistrer le type
add_filter('product_type_selector', function ($types) {
$types['subscription_box'] = __('Subscription Box', 'atum');
return $types;
});
// 2. Créer la classe
class WC_Product_Subscription_Box extends WC_Product {
public function get_type() {
return 'subscription_box';
}
public function get_frequency() {
return $this->get_meta('_sb_frequency', true);
}
}
// 3. Mapper le type au class
add_filter('woocommerce_product_class', function ($classname, $product_type) {
if ($product_type === 'subscription_box') {
return 'WC_Product_Subscription_Box';
}
return $classname;
}, 10, 2);
// Préférer wc_get_products() à WP_Query pour HPOS-compat
$products = wc_get_products([
'status' => 'publish',
'limit' => 10,
'orderby' => 'date',
'order' => 'DESC',
'category' => ['featured'],
'tax_query' => [
[
'taxonomy' => 'product_visibility',
'field' => 'name',
'terms' => 'exclude-from-catalog',
'operator' => 'NOT IN',
],
],
]);
foreach ($products as $product) {
echo $product->get_name() . "\n";
}
add_action('woocommerce_before_cart', function () {
if (WC()->cart->get_cart_contents_count() >= 3) {
wc_print_notice(
__('Free shipping unlocked!', 'atum'),
'success'
);
}
});
add_action('woocommerce_cart_calculate_fees', function () {
if (is_admin() && !defined('DOING_AJAX')) {
return;
}
$total = WC()->cart->get_subtotal();
if ($total < 50) {
WC()->cart->add_fee(__('Small order fee', 'atum'), 2.5, true);
}
});
add_filter('woocommerce_checkout_fields', function ($fields) {
$fields['billing']['billing_company_vat'] = [
'label' => __('VAT number', 'atum'),
'placeholder' => 'FR12345678901',
'required' => false,
'class' => ['form-row-wide'],
'priority' => 35,
];
return $fields;
});
add_action('woocommerce_checkout_process', function () {
if (!empty($_POST['billing_company_vat'])) {
$vat = sanitize_text_field($_POST['billing_company_vat']);
if (!preg_match('/^[A-Z]{2}[0-9A-Z]{2,12}$/', $vat)) {
wc_add_notice(__('Invalid VAT number format.', 'atum'), 'error');
}
}
});
add_action('woocommerce_checkout_create_order', function ($order, $data) {
if (!empty($_POST['billing_company_vat'])) {
$order->update_meta_data(
'_billing_company_vat',
sanitize_text_field($_POST['billing_company_vat'])
);
}
}, 10, 2);
Note HPOS : utiliser les méthodes de WC_Order (update_meta_data, get_meta, save) au lieu de update_post_meta — ce dernier ne fonctionne pas avec HPOS.
add_filter('woocommerce_payment_gateways', function ($gateways) {
$gateways[] = 'WC_Gateway_MyProvider';
return $gateways;
});
add_action('plugins_loaded', function () {
if (!class_exists('WC_Payment_Gateway')) {
return;
}
class WC_Gateway_MyProvider extends WC_Payment_Gateway {
public function __construct() {
$this->id = 'my_provider';
$this->method_title = __('My Provider', 'atum');
$this->method_description = __('Accept payments via My Provider', 'atum');
$this->has_fields = true;
$this->supports = ['products', 'refunds'];
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option('title');
$this->description = $this->get_option('description');
$this->api_key = $this->get_option('api_key');
add_action('woocommerce_update_options_payment_gateways_' . $this->id, [$this, 'process_admin_options']);
}
public function init_form_fields() {
$this->form_fields = [
'enabled' => [
'title' => __('Enable/Disable', 'atum'),
'type' => 'checkbox',
'label' => __('Enable My Provider', 'atum'),
'default' => 'yes',
],
'title' => [
'title' => __('Title', 'atum'),
'type' => 'text',
'description' => __('Payment method title on the checkout page', 'atum'),
'default' => __('Pay with My Provider', 'atum'),
],
'api_key' => [
'title' => __('API Key', 'atum'),
'type' => 'password',
'description' => __('Your My Provider API key', 'atum'),
],
];
}
public function process_payment($order_id) {
$order = wc_get_order($order_id);
// Appel API au provider
$response = wp_remote_post('https://api.myprovider.com/charge', [
'headers' => [
'Authorization' => 'Bearer ' . $this->api_key,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode([
'amount' => (int) ($order->get_total() * 100),
'currency' => $order->get_currency(),
'order_id' => $order->get_id(),
]),
'timeout' => 30,
]);
if (is_wp_error($response)) {
wc_add_notice(__('Payment failed. Please try again.', 'atum'), 'error');
return ['result' => 'failure'];
}
$body = json_decode(wp_remote_retrieve_body($response), true);
if (empty($body['success'])) {
wc_add_notice(
sprintf(__('Payment error: %s', 'atum'), $body['error'] ?? 'unknown'),
'error'
);
return ['result' => 'failure'];
}
$order->payment_complete($body['transaction_id']);
$order->add_order_note(sprintf(__('Payment via My Provider. Transaction: %s', 'atum'), $body['transaction_id']));
WC()->cart->empty_cart();
return [
'result' => 'success',
'redirect' => $this->get_return_url($order),
];
}
}
});
PSD2/SCA : Pour l'Europe, implémenter 3D-Secure via redirection vers le provider qui gère le challenge bancaire. Ne jamais contourner SCA.
add_filter('woocommerce_shipping_methods', function ($methods) {
$methods['express_courier'] = 'WC_Shipping_Express_Courier';
return $methods;
});
add_action('woocommerce_shipping_init', function () {
if (!class_exists('WC_Shipping_Method')) {
return;
}
class WC_Shipping_Express_Courier extends WC_Shipping_Method {
public function __construct($instance_id = 0) {
$this->id = 'express_courier';
$this->instance_id = absint($instance_id);
$this->method_title = __('Express Courier', 'atum');
$this->method_description = __('Next-day delivery via our courier partner', 'atum');
$this->supports = ['shipping-zones', 'instance-settings'];
$this->init();
}
public function init() {
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option('title');
$this->cost = (float) $this->get_option('cost', 10);
add_action('woocommerce_update_options_shipping_' . $this->id, [$this, 'process_admin_options']);
}
public function init_form_fields() {
$this->instance_form_fields = [
'title' => [
'title' => __('Method title', 'atum'),
'type' => 'text',
'description' => __('Name shown to the customer', 'atum'),
'default' => __('Express (next day)', 'atum'),
],
'cost' => [
'title' => __('Cost', 'atum'),
'type' => 'number',
'description' => __('Flat fee', 'atum'),
'default' => '10',
],
];
}
public function calculate_shipping($package = []) {
$this->add_rate([
'id' => $this->get_rate_id(),
'label' => $this->title,
'cost' => $this->cost,
]);
}
}
});
# GET products avec basic auth (consumer key + secret)
curl -u ck_xxx:cs_xxx \
"https://mysite.com/wp-json/wc/v3/products?per_page=10"
# GET products pour afficher sur un frontend headless
curl "https://mysite.com/wp-json/wc/store/v1/products"
# Cart et checkout sont également disponibles
curl "https://mysite.com/wp-json/wc/store/v1/cart"
Utiliser la Store API pour Next.js headless :
// lib/wc-store.ts
export async function getProducts() {
const res = await fetch(`${process.env.WC_URL}/wp-json/wc/store/v1/products?per_page=20`, {
next: { revalidate: 60 },
})
return res.json()
}
Créer un webhook via l'admin WooCommerce (Settings → Advanced → Webhooks) ou programmatically :
$webhook = new WC_Webhook();
$webhook->set_name('Order created to external system');
$webhook->set_topic('order.created');
$webhook->set_delivery_url('https://my-backend.com/webhooks/woocommerce');
$webhook->set_secret(wp_generate_password(32, false));
$webhook->set_status('active');
$webhook->save();
Vérifier la signature côté récepteur :
// Next.js webhook handler
import crypto from 'node:crypto'
export async function POST(request: Request) {
const rawBody = await request.text()
const signature = request.headers.get('x-wc-webhook-signature') ?? ''
const expected = crypto
.createHmac('sha256', process.env.WC_WEBHOOK_SECRET!)
.update(rawBody)
.digest('base64')
if (expected !== signature) {
return new Response('invalid signature', { status: 401 })
}
const payload = JSON.parse(rawBody)
// ...
}
get_post_meta($order_id, ...) sur un order → ne fonctionne pas avec HPOS, utiliser $order->get_meta()wp_insert_post pour créer une commande → utiliser wc_create_order()$_POST lu sans nonce dans un hook checkout → CSRFwc_update_product_stock() ou laisser WC gérerorder.created, order.updated, product.updatedno_found_rows => true si non utilisée)wordpress-patterns (dans ce plugin)wordpress-expert (dans ce plugin)cms-headless-architecture + command /cms-compareshopify-expert (dans ce plugin)security-reviewerdatabase-reviewer pour les slow queries