# 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/ │ │ ├── ProductData.php # Product data tab & meta boxes │ │ └── Settings.php # WooCommerce settings integration │ ├── CartHandler.php # Add-to-cart & cart display logic (with stock validation) │ ├── Plugin.php # Main plugin class (Singleton) │ ├── ProductSelector.php # Frontend product selector renderer (with stock info) │ ├── ProductType.php # Custom WC_Product extension │ └── StockManager.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. **ProductType.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. **CartHandler.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. **ProductSelector.php** — Frontend renderer - Renders Twig template with product data, stock info, and pre-formatted price HTML via `wc_price()` 5. **Admin/ProductData.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. **StockManager.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:** `CartHandler::render_product_selector()` → `ProductType::get_available_products()` queries products via taxonomy/SKU → `ProductSelector::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 → `StockManager` 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