Files
wc-composable-product/CLAUDE.md

187 lines
9.2 KiB
Markdown
Raw Normal View History

# WooCommerce Composable Products - AI Context Document
**Author:** Marco Graetsch
## Project Overview
This plugin implements a custom WooCommerce product type where customers select a limited number of products from a configurable set of simple or variable products. The limit is configurable globally and per-product. The selectable products are defined by category, tag, or SKU. Pricing is either fixed or the sum of selected products. Think of a sticker pack where each package contains N stickers chosen by the customer.
This project is 100% AI-generated ("vibe-coded") using Claude.AI.
## Technical Stack
- **Language:** PHP 8.3+
- **Framework:** WordPress Plugin API
- **E-commerce:** WooCommerce 10.0+
- **Template Engine:** Twig 3.0 (via Composer)
- **Frontend:** Vanilla JavaScript + jQuery
- **Styling:** Custom CSS
- **Dependencies:** Composer
- **i18n:** WordPress i18n (.pot/.po/.mo), text domain: `wc-composable-product`
- **CI/CD:** Gitea Actions (`.gitea/workflows/release.yml`)
## Project Structure
```txt
wc-composable-product/
├── .gitea/workflows/
│ └── release.yml # CI/CD release workflow
├── assets/
│ ├── css/
│ │ ├── admin.css # Admin panel styling
│ │ └── frontend.css # Customer-facing styles (with stock indicators)
│ └── js/
│ ├── admin.js # Product edit interface logic
│ └── frontend.js # AJAX cart & selection UI
├── cache/ # Twig template cache (writable, gitignored)
├── includes/
│ ├── Admin/
│ │ ├── Product_Data.php # Product data tab & meta boxes
│ │ └── Settings.php # WooCommerce settings integration
│ ├── Cart_Handler.php # Add-to-cart & cart display logic (with stock validation)
│ ├── Plugin.php # Main plugin class (Singleton)
│ ├── Product_Selector.php # Frontend product selector renderer (with stock info)
│ ├── Product_Type.php # Custom WC_Product extension
│ └── Stock_Manager.php # Stock management & inventory tracking
├── languages/ # Translation files (.pot, .po, .mo)
├── releases/ # Release packages (gitignored)
├── templates/
│ └── product-selector.twig # Frontend selection interface
├── vendor/ # Composer dependencies (gitignored, included in releases)
├── composer.json
└── wc-composable-product.php # Main plugin file
```
## Architecture
### Core Classes
1. **Plugin.php** — Main singleton class
- Initializes Twig with WordPress functions registered as both Twig functions AND filters
- Registers hooks, manages asset enqueuing, provides template rendering API
- Settings.php is lazy-loaded via `woocommerce_get_settings_pages` filter (not in `includes()`) to avoid "Class WC_Settings_Page not found" errors
2. **Product_Type.php** — Custom WooCommerce product type (`composable`)
- Extends `WC_Product`
- Queries available products via `get_available_products()` using `WP_Query`
- **Critical**: Uses `tax_query` with `product_type` taxonomy to exclude composable products (NOT `meta_query` — WooCommerce stores product types as taxonomy terms)
- Handles variable products by expanding them into individual variations via `get_children()`
- Products are filtered by `is_purchasable()` only (not `is_in_stock()` — stock is shown visually and validated at add-to-cart)
3. **Cart_Handler.php** — Cart integration
- Validates selections, stores selected products in cart meta, calculates pricing
- Uses `woocommerce_is_purchasable` filter to hide default add-to-cart button for composable products
- Price recalculation uses a static `$already_calculated` flag per request (no persistent session flags — `set_price()` is in-memory only)
4. **Product_Selector.php** — Frontend renderer
- Renders Twig template with product data, stock info, and pre-formatted price HTML via `wc_price()`
5. **Admin/Product_Data.php** — Product edit interface
- Adds "Composable Options" tab with category/tag/SKU selection fields
- Saved meta: `_composable_selection_limit`, `_composable_pricing_mode`, `_composable_criteria_type`, `_composable_categories`, `_composable_tags`, `_composable_skus`
6. **Admin/Settings.php** — Global settings (extends `WC_Settings_Page`)
- Default selection limit, pricing mode, display preferences
7. **Stock_Manager.php** — Inventory management
- Stock validation, automatic deduction on order completion, restoration on cancellation
- Prevents WooCommerce double-deduction via `woocommerce_can_reduce_order_stock`
### Data Flow
**Product Creation:** Admin selects "Composable product" type → configures criteria/limits/pricing → metadata saved as `_composable_*` fields
**Frontend Display:** `Cart_Handler::render_product_selector()``Product_Type::get_available_products()` queries products via taxonomy/SKU → `Product_Selector::render()` passes data to Twig template → JavaScript handles selection UI
**Add to Cart:** Customer selects products → JS validates limit → AJAX request with `composable_products[]` → server-side validation (selection + stock) → selections stored in cart item data → price calculated per pricing mode
**Order Processing:** Order completed → `Stock_Manager` deducts inventory → order notes added for audit → on cancellation/refund: stock restored
### Key Hooks
- `woocommerce_add_to_cart_validation` — validate selections
- `woocommerce_add_cart_item_data` — store selections
- `woocommerce_before_calculate_totals` — update prices
- `woocommerce_get_item_data` — display in cart
- `woocommerce_order_status_completed/processing` — deduct stock
- `woocommerce_order_status_cancelled/refunded` — restore stock
### Security
- Input: `absint()` for IDs/limits, `sanitize_text_field()` for modes, `sanitize_textarea_field()` for SKUs
- Output: `esc_html()`, `esc_attr()`, `esc_url()` (registered as both Twig functions and filters)
- Nonce verification via WooCommerce
### Developer API
```php
$product = wc_get_product($product_id);
if ($product->get_type() === 'composable') {
$products = $product->get_available_products();
$limit = $product->get_selection_limit();
$price = $product->calculate_composed_price($selected_ids);
}
```
## Translations
All strings use text domain `wc-composable-product`. Available locales:
- `en_US` (base), `de_DE`, `de_DE_informal`, `de_CH`, `de_CH_informal`, `fr_CH`, `it_CH`
Compile .po to .mo: `for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done`
WordPress requires compiled .mo files — .po files alone are insufficient.
## Release Workflow
### Automated (Gitea CI/CD)
Push an annotated tag (`v*`) to trigger the workflow. It installs PHP 8.3, production Composer deps, compiles translations, verifies version matches tag, creates ZIP with checksums, and publishes a Gitea release.
### Manual
```bash
# From project root
zip -r releases/wc-composable-product-vX.X.X.zip . \
-x "*.git*" "*.vscode*" "*.claude*" "CLAUDE.md" \
"wp-core/*" "wp-plugins/*" "*.log" "composer.lock" \
"cache/*" "releases/*" "*.zip" "logs/*"
```
The `vendor/` directory MUST be included in releases (Twig dependency required at runtime).
### Git Workflow
- Develop on `dev` branch, merge to `main` for releases
- Tags: annotated, format `vX.X.X` (e.g., `v1.2.0`)
- Commit messages include `Co-Authored-By: Claude` attribution
## Critical Lessons Learned
1. **WooCommerce stores product types as taxonomy terms** (`product_type` taxonomy), NOT as postmeta. Using `meta_query` on `_product_type` silently returns zero results because the meta key doesn't exist.
2. **`WC_Product::set_price()` is in-memory only** — changes are lost between HTTP requests. Never persist a "price already calculated" flag to cart session; use a static per-request flag instead.
3. **Settings.php must be lazy-loaded**`require_once` in `Plugin::includes()` causes "Class WC_Settings_Page not found" because WooCommerce hasn't loaded that class yet. Load it inside the `woocommerce_get_settings_pages` filter callback instead.
4. **Register WordPress functions as both Twig functions AND filters** — other plugins may bundle their own Twig instance that parses our templates. Both `{{ esc_attr(value) }}` and `{{ value|esc_attr }}` syntax must work.
5. **HPOS compatibility declaration is required** — without it, WooCommerce shows incompatibility warnings.
6. **WordPress i18n requires compiled .mo files** — .po files are source only; WordPress cannot use them directly.
7. **Don't filter by `is_in_stock()` during product retrieval** — it's too strict (excludes backorder-enabled products, products without stock management). Show all purchasable products; let the frontend display stock status and validate at add-to-cart time.
## For AI Assistants
When starting a new session:
1. Read this CLAUDE.md first
2. Check git log for recent changes
3. Verify you're on the `dev` branch before making changes
4. Run `composer install` if vendor/ is missing
5. Test changes before committing
6. Follow commit message format with Claude Code attribution
7. Always use `tax_query` (not `meta_query`) for product type filtering