# Wc Cart Checkout Classic

> Customize the classic WooCommerce cart and shortcode checkout with `woocommerce_add_cart_item_data`, `woocommerce_get_item_data`, `woocommerce_before_calculate_totals`, `woocommerce_cart_calculate_fees`, `woocommerce_checkout_fields`, `woocommerce_after_checkout_validation`, `woocommerce_checkout_update_order_meta`, and `woocommerce_checkout_create_order_line_item`. Covers cart-key merging, absolute price mutation, fees, classic checkout fields, order-line meta vs order meta, HPOS-safe order saves, and the Checkout Block / Store API boundary. Use when adding product options, custom cart data, fees, classic checkout fields, validation, or debugging missing/duplicated cart/order item data.

- Skill: `vikingokft/wc-cart-checkout-classic` (Agent Skill)
- Install (CLI): `npx skillmds add vikingokft/wc-cart-checkout-classic`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vikingokft/wc-cart-checkout-classic/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: vikingokft (https://skillmd.com/u/vikingokft)
- Updated: 2026-09-08
- Page: https://skillmd.com/skills/vikingokft/wc-cart-checkout-classic

---


# WooCommerce classic cart and checkout

Use this for the PHP/classic cart and shortcode checkout flow. It covers product add-to-cart customization, cart item data, calculated prices/fees, checkout fields, checkout validation, and copying cart data to orders.

It is not a Checkout Block UI skill. Some low-level cart hooks also run during Store API requests, but `woocommerce_checkout_fields` does not make fields appear in the Checkout Block. For block/headless cart state, use `wc-store-api`.

## Misconception this skill corrects

> "I added custom cart item data, so WooCommerce will automatically show it on the order."

Cart item data affects the cart key and lives in the cart/session. It is not automatically saved as order line-item meta. Display it with `woocommerce_get_item_data`, and copy it to the order line with `woocommerce_checkout_create_order_line_item`.

## When to use this skill

Trigger when ANY of the following is true:

- Adding a product option from an add-to-cart form.
- Storing custom data on a cart item.
- Showing custom data in cart/checkout/order line items.
- Changing cart item price dynamically.
- Adding a handling, insurance, gift-wrap, or payment-related fee.
- Adding fields to classic checkout.
- Validating checkout data server-side.
- The diff contains `woocommerce_add_cart_item_data`, `woocommerce_get_item_data`, `woocommerce_before_calculate_totals`, `woocommerce_cart_calculate_fees`, `woocommerce_checkout_fields`, `woocommerce_after_checkout_validation`, or `woocommerce_checkout_create_order_line_item`.

## Cart item identity

`WC_Cart::add_to_cart()` applies `woocommerce_add_cart_item_data` before generating the cart ID. `WC_Cart::generate_cart_id()` includes product ID, variation ID, variation attributes, and every value in `$cart_item_data`.

That means:

- If two cart additions have the same product, variation, and cart item data, Woo merges quantities into one cart line.
- If cart item data differs, Woo creates a different cart line.
- Do not add a random unique value unless you intentionally want every add-to-cart click to be a separate line.

```php
add_filter(
    'woocommerce_add_cart_item_data',
    static function ( array $cart_item_data, int $product_id, int $variation_id, int $quantity ): array {
        if ( empty( $_POST['myplugin_engraving'] ) ) {
            return $cart_item_data;
        }

        $engraving = sanitize_text_field( wp_unslash( $_POST['myplugin_engraving'] ) );
        if ( '' === $engraving ) {
            return $cart_item_data;
        }

        $cart_item_data['myplugin_engraving'] = $engraving;

        // Only add this if identical configured items must never merge:
        // $cart_item_data['myplugin_line_uid'] = wp_generate_uuid4();

        return $cart_item_data;
    },
    10,
    4
);
```

The classic add-to-cart form path also applies `woocommerce_add_to_cart_validation`. Use it to reject invalid posted product options before the cart line is created.

## Display cart item data

`woocommerce_get_item_data` feeds `wc_get_formatted_cart_item_data()`, which is used by cart and checkout templates.

```php
add_filter(
    'woocommerce_get_item_data',
    static function ( array $item_data, array $cart_item ): array {
        if ( empty( $cart_item['myplugin_engraving'] ) ) {
            return $item_data;
        }

        $item_data[] = array(
            'name'  => __( 'Engraving', 'myplugin' ),
            'value' => esc_html( $cart_item['myplugin_engraving'] ),
        );

        return $item_data;
    },
    10,
    2
);
```

This is display only. It does not persist to the order.

## Copy cart data to order lines

Use `woocommerce_checkout_create_order_line_item` for per-item meta. Do not put line-item data into `woocommerce_checkout_update_order_meta`; that hook is order-level.

```php
add_action(
    'woocommerce_checkout_create_order_line_item',
    static function ( WC_Order_Item_Product $item, string $cart_item_key, array $values, WC_Order $order ): void {
        if ( empty( $values['myplugin_engraving'] ) ) {
            return;
        }

        $item->add_meta_data(
            __( 'Engraving', 'myplugin' ),
            sanitize_text_field( $values['myplugin_engraving'] ),
            true
        );
    },
    10,
    4
);
```

Use a private meta key such as `_myplugin_config` if the value is for machine logic only. Use a readable label when the value should appear to admins/customers.

## Dynamic cart item prices

`woocommerce_before_calculate_totals` runs whenever Woo recalculates totals. Set an absolute price every time; do not add to the current price repeatedly.

Store the base value when the cart item is created:

```php
add_filter(
    'woocommerce_add_cart_item_data',
    static function ( array $cart_item_data, int $product_id, int $variation_id ): array {
        $product = wc_get_product( $variation_id ?: $product_id );
        if ( $product instanceof WC_Product ) {
            $cart_item_data['myplugin_base_price'] = (float) $product->get_price( 'edit' );
        }
        return $cart_item_data;
    },
    20,
    3
);
```

Then set the calculated price:

```php
add_action(
    'woocommerce_before_calculate_totals',
    static function ( WC_Cart $cart ): void {
        if ( is_admin() && ! wp_doing_ajax() ) {
            return;
        }

        foreach ( $cart->get_cart() as $cart_item ) {
            if ( empty( $cart_item['myplugin_engraving'] ) || ! isset( $cart_item['myplugin_base_price'] ) ) {
                continue;
            }

            $cart_item['data']->set_price( (float) $cart_item['myplugin_base_price'] + 5.00 );
        }
    },
    20
);
```

Do not call `update_post_meta()` or product setters that save the product here. The product object in the cart line is a runtime object; the catalog product price should not be changed.

## Fees

Use `woocommerce_cart_calculate_fees` for cart-level fees. Do not add fees in `woocommerce_before_calculate_totals`.

```php
add_action(
    'woocommerce_cart_calculate_fees',
    static function ( WC_Cart $cart ): void {
        if ( is_admin() && ! wp_doing_ajax() ) {
            return;
        }

        if ( $cart->is_empty() ) {
            return;
        }

        $cart->add_fee( __( 'Handling', 'myplugin' ), 5.00, true, '' );
    }
);
```

Fees become `WC_Order_Item_Fee` items during checkout. They are not product line items.

## Classic checkout fields

`woocommerce_checkout_fields` modifies the field arrays for classic checkout sections: `billing`, `shipping`, `account`, and `order`.

```php
add_filter(
    'woocommerce_checkout_fields',
    static function ( array $fields ): array {
        $fields['billing']['billing_vat_id'] = array(
            'type'        => 'text',
            'label'       => __( 'VAT ID', 'myplugin' ),
            'required'    => false,
            'priority'    => 120,
            'autocomplete'=> 'off',
        );

        return $fields;
    }
);

add_action(
    'woocommerce_after_checkout_validation',
    static function ( array $data, WP_Error $errors ): void {
        if ( empty( $data['billing_vat_id'] ) ) {
            return;
        }

        if ( ! preg_match( '/^[A-Z0-9 -]{4,32}$/i', (string) $data['billing_vat_id'] ) ) {
            $errors->add( 'billing_vat_id', __( 'Enter a valid VAT ID.', 'myplugin' ) );
        }
    },
    10,
    2
);

add_action(
    'woocommerce_checkout_update_order_meta',
    static function ( int $order_id, array $data ): void {
        if ( empty( $data['billing_vat_id'] ) ) {
            return;
        }

        $order = wc_get_order( $order_id );
        if ( ! $order instanceof WC_Order ) {
            return;
        }

        $order->update_meta_data( '_billing_vat_id', sanitize_text_field( $data['billing_vat_id'] ) );
        $order->save();
    },
    10,
    2
);
```

Use Woo order APIs for HPOS compatibility. Never write checkout order data with `update_post_meta( $order_id, ... )`.

## Common mistakes

- Adding random cart item data unintentionally prevents quantity merging.
- Changing `$product->set_price( $product->get_price() + 5 )` in every totals calculation compounds the price.
- Saving product objects from cart hooks changes catalog data.
- Adding fees from `woocommerce_before_calculate_totals` instead of `woocommerce_cart_calculate_fees`.
- Expecting `woocommerce_checkout_fields` to render in Checkout Block.
- Saving line-item data in order meta instead of `woocommerce_checkout_create_order_line_item`.
- Trusting posted product/checkout fields without sanitizing and validating.
- Using `$_SESSION`; use `WC()->session` for cart/session state.

## Cross-skill routing

- Checkout Block, Store API cart, headless checkout: `wc-store-api`
- HPOS order storage concerns: `wc-hpos-compatibility`
- Customer/session persistence: `wc-customer-and-sessions`
- Payment gateway checkout processing: `wc-payment-gateway`

