# WooCommerce Tier and Package Prices - AI Context Document **Last Updated:** 2026-01-27 **Current Version:** 1.3.1 **Author:** Marco Graetsch **Project Status:** Production-ready WordPress plugin ## Project Overview This is a WooCommerce plugin that adds flexible pricing capabilities to products through two distinct pricing models: 1. **Tier Pricing (Volume Discounts)**: Progressive discounts based on quantity ranges (e.g., 1-9 items @ $12, 10-24 @ $10, 25+ @ $8) 2. **Package Pricing (Fixed Bundles)**: Exact quantity packages at fixed prices (e.g., exactly 10 items for $95, exactly 25 for $200) ### Key Fact: 100% AI-Generated This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase was created through AI assistance. ## Temporary Roadmap **Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file. Always keep the `Known Bugs` section and create a section with the next bugfix and minor version after a release. ### Version 1.3.2 - No planned changes yet ## Technical Stack - **Language:** PHP 8.3+ - **Framework:** WordPress Plugin API - **E-commerce:** WooCommerce 8.0+ (tested up to 10.x) - **Template Engine:** Twig 3.0 (via Composer) - **Frontend:** Vanilla JavaScript + jQuery - **Styling:** Custom CSS - **Dependency Management:** Composer - **Internationalization:** WordPress i18n (.pot/.po/.mo files) ### Dependencies ```json { "twig/twig": "^3.0", "magdev/wc-licensed-product-client": "dev-main", "symfony/http-client": "^7.0", "psr/log": "^3.0", "psr/cache": "^3.0", "psr/http-client": "^1.0" } ``` ## Architecture ### Directory Structure ```txt wc-tier-and-package-prices/ ├── wc-tier-and-package-prices.php # Main plugin file (entry point) ├── includes/ # PHP classes │ ├── class-wc-tpp-admin.php # Admin settings integration │ ├── class-wc-tpp-settings.php # WooCommerce settings page │ ├── class-wc-tpp-product-meta.php # Product edit page meta boxes │ ├── class-wc-tpp-frontend.php # Product page display logic │ ├── class-wc-tpp-cart.php # Cart price calculations │ └── class-wc-tpp-template-loader.php # Twig template loader ├── templates/ # Twig templates │ ├── admin/ # Admin interface templates │ │ ├── tier-row.twig # Single tier input row │ │ └── package-row.twig # Single package input row │ └── frontend/ # Customer-facing templates │ ├── pricing-table.twig # Main pricing display wrapper │ ├── tier-pricing-table.twig # Tier pricing display │ └── package-pricing-display.twig # Package buttons/cards ├── assets/ │ ├── css/ │ │ ├── admin.css # Backend styling │ │ └── frontend.css # Product page & cart styling │ └── js/ │ ├── admin.js # Meta box interaction (add/remove rows) │ └── frontend.js # Dynamic price updates, package selection ├── languages/ # Translation files │ ├── *.pot # Translation template │ ├── *.po # Translation sources │ └── *.mo # Compiled translations ├── vendor/ # Composer dependencies (included in releases) ├── releases/ # Release packages (not in git) └── *.md # Documentation files ``` ### Class Responsibilities #### 1. `WC_Tier_Package_Prices` (Main Plugin Class) - **Location:** `wc-tier-and-package-prices.php` - **Pattern:** Singleton - **Responsibilities:** - Plugin initialization and bootstrapping - Loading all component classes via `includes()` - HPOS (High-Performance Order Storage) compatibility declaration - Text domain loading for internationalization - Activation/deactivation hooks #### 2. `WC_TPP_Admin` - **Location:** `includes/class-wc-tpp-admin.php` - **Pattern:** Singleton - **Responsibilities:** - Enqueues admin CSS/JS - Registers WooCommerce settings page via filter - Manages settings page instance (cached to prevent duplicates) - Product meta box asset loading #### 3. `WC_TPP_Settings` - **Location:** `includes/class-wc-tpp-settings.php` - **Extends:** `WC_Settings_Page` (WooCommerce core) - **Responsibilities:** - Creates "Tier & Package Prices" tab in WooCommerce settings - Defines global plugin settings (enable/disable features, display position, etc.) - Setting persistence through WooCommerce options API **Global Settings:** - `wc_tpp_enable_tier_pricing` (yes/no) - `wc_tpp_enable_package_pricing` (yes/no) - `wc_tpp_display_table` (yes/no) - Show pricing tables on product pages - `wc_tpp_display_position` (before_add_to_cart / after_add_to_cart / after_price) - `wc_tpp_restrict_package_quantities` (yes/no) - Global quantity restrictions #### 4. `WC_TPP_Product_Meta` - **Location:** `includes/class-wc-tpp-product-meta.php` - **Responsibilities:** - Adds tier/package pricing fields to product edit page - Renders Twig templates for meta box rows - Saves tier/package data to post meta - Nonce verification and capability checks for security - Prevents autosave from corrupting data **Product Meta Keys:** - `_wc_tpp_tiers` - Array of tier objects `[{min_qty, price, label}]` - `_wc_tpp_packages` - Array of package objects `[{qty, price, label}]` - `_wc_tpp_restrict_to_packages` - Per-product quantity restriction (yes/no) #### 5. `WC_TPP_Frontend` - **Location:** `includes/class-wc-tpp-frontend.php` - **Responsibilities:** - Enqueues frontend CSS/JS on product pages - Displays pricing tables via Twig templates - Localizes currency settings to JavaScript - Hides quantity inputs for restricted products - Modifies catalog "Add to Cart" buttons to "View Options" for restricted products - Static methods for price lookups (`get_tier_price()`, `get_package_price()`) #### 6. `WC_TPP_Cart` - **Location:** `includes/class-wc-tpp-cart.php` - **Responsibilities:** - **MOST CRITICAL CLASS** - Handles all cart price calculations - Applies tier/package pricing during cart totals calculation - Stores pricing metadata in cart items for display - Customizes cart item display (price labels, quantity indicators) - Validates package quantities on add-to-cart - Hides/disables quantity inputs for restricted products (classic cart + blocks) - **WooCommerce Blocks support** via `woocommerce_store_api_product_quantity_editable` filter **Price Calculation Priority (in `apply_tier_package_pricing()`):** 1. Check for exact package match → Use package price if found 2. Check for tier match → Use tier price if found 3. Fall back to regular product price #### 7. `WC_TPP_Template_Loader` - **Location:** `includes/class-wc-tpp-template-loader.php` - **Pattern:** Singleton - **Responsibilities:** - Initializes Twig environment with proper paths - Renders Twig templates from both admin and frontend directories - Handles template caching and error handling ## Important Implementation Details ### Price Calculation Logic **Package Pricing** (exact match): ```php // In cart: if quantity == 10 and package exists for 10, use package price if ($quantity == $package['qty']) { $unit_price = $package['price'] / $quantity; // Total price divided by quantity $product->set_price($unit_price); // WooCommerce expects unit price } ``` **Tier Pricing** (range-based): ```php // In cart: if quantity >= 10, use tier price for quantities 10+ foreach ($tiers as $tier) { if ($quantity >= $tier['min_qty']) { $applicable_price = $tier['price']; // This is already unit price } } $product->set_price($applicable_price); ``` ### Quantity Restriction Feature Products can be configured to ONLY allow purchase in package quantities: - **Global setting:** `wc_tpp_restrict_package_quantities` - **Per-product setting:** `_wc_tpp_restrict_to_packages` - **When enabled:** - Quantity inputs are hidden on product page, cart, and mini-cart - Customers must use package selection buttons - Validation prevents arbitrary quantities from being added - Catalog buttons change to "View Options" instead of "Add to Cart" ### WooCommerce Blocks Compatibility **CRITICAL BUG FIXED in v1.1.20:** - Filter `woocommerce_store_api_product_quantity_editable` passes `WC_Product` object, NOT cart item array - Previous code tried to use product object as array → fatal error - Fixed by accepting product object and using `$product->get_id()` ### Cart Item Metadata The plugin stores additional data in cart items for display purposes: ```php WC()->cart->cart_contents[$cart_item_key]['wc_tpp_pricing_type'] = 'package' | 'tier'; WC()->cart->cart_contents[$cart_item_key]['wc_tpp_total_price'] = 99.99; // For packages WC()->cart->cart_contents[$cart_item_key]['wc_tpp_unit_price'] = 9.99; // For tiers ``` This metadata is used by display filters to show "(Package price)" or "(Volume discount)" labels. ## Common Patterns & Conventions ### Class Instantiation Pattern All classes auto-instantiate at the end of their file: ```php if (!class_exists('WC_TPP_Frontend')) { class WC_TPP_Frontend { // class code } } new WC_TPP_Frontend(); // Auto-instantiate ``` **Exception:** Admin and Settings classes use singleton pattern to prevent duplicates. ### Security Best Practices - All user inputs are sanitized (integers for quantities/prices) - Nonce verification on form submissions - Capability checks (`edit_products`) before saving - Output escaping in templates (`esc_attr`, `esc_html`, `esc_js`) - Direct file access prevention via `ABSPATH` check ### Translation Ready All user-facing strings use: ```php __('Text to translate', 'wc-tier-package-prices') _e('Text to translate', 'wc-tier-package-prices') ``` Text domain: `wc-tier-package-prices` **Available Translations (as of v1.1.22):** - `en_US` - English (United States) - `de_DE` - German (Germany, formal) - `de_DE_informal` - German (Germany, informal "du") - `de_CH` - German (Switzerland, formal "Sie") - `de_CH_informal` - German (Switzerland, informal "du") - `fr_CH` - French (Switzerland) - `it_CH` - Italian (Switzerland) Note: Swiss locales use CHF currency formatting in examples (e.g., "CHF 50.-") ## Known Issues & Historical Context ### Settings Page Duplication Saga (v1.1.15-1.1.19) Multiple versions attempted to fix settings page appearing twice: - **Root cause:** Settings file auto-instantiation + Composer autoloader - **Solution:** Removed auto-instantiation from settings file, explicit instantiation in admin class - **Prevention:** Singleton pattern + duplicate detection in array ### Class Redeclaration Issues (v1.1.8-1.1.14) Plugin was completely non-functional: - **Cause:** Incorrect initialization pattern without `class_exists()` guards - **Solution:** Added guards and restored direct instantiation pattern - **Lesson:** Always wrap class declarations in `class_exists()` checks ### WooCommerce Blocks Fatal Error (v1.1.19 → v1.1.20) ```txt Fatal error: Cannot use object of type WC_Product_Simple as array Location: includes/class-wc-tpp-cart.php:233 ``` - **Cause:** Filter signature mismatch - expected array, received product object - **Fix:** Changed method signature to accept `WC_Product $product` instead of `$cart_item` array - **Status:** FIXED in v1.1.20 ### CSS Specificity Issues (v1.2.3 → v1.2.4) **Problem:** Admin table borders still visible despite `border: none` declarations in v1.2.3 ```txt Issue: WooCommerce's core admin CSS has higher specificity border rules Location: assets/css/admin.css Symptom: Tables still showing borders in product edit screens ``` - **Root Cause:** WooCommerce's default admin CSS uses highly specific selectors that override simple `border: none` declarations - **Failed Approach (v1.2.3):** Adding `border: none` to table elements without `!important` - **Successful Fix (v1.2.4):** - Added `!important` flags to ALL border removal rules - Added `border-collapse: collapse !important` to force borderless styling - Targeted all table structural elements: `table`, `th`, `td`, `thead`, `tbody`, `tr` - **Lesson:** When overriding WooCommerce core CSS, `!important` is often necessary due to high specificity in core styles **Problem:** Help icon positioned at right edge instead of next to label text ```txt Issue: WooCommerce help-tip uses float: right positioning Location: assets/css/admin.css (checkbox/label layout) Symptom: Help icon appearing far from label text at container edge ``` - **Root Cause:** WooCommerce's default `.woocommerce-help-tip` styling uses `float: right` - **Failed Approach (v1.2.3):** Simple margin adjustments without changing positioning model - **Successful Fix (v1.2.4):** - Removed float positioning: `float: none` - Changed to `display: inline-block` with `vertical-align: middle` - Wrapped label and help-tip in flexbox container: `display: inline-flex; align-items: center` - Controlled precise spacing with margins (checkbox: 12px, help-tip: 6px) - **Lesson:** Overriding float-based layouts often requires changing to flexbox for proper control ## Release Process ### Version Bumping Update version in 3 places: 1. `wc-tier-and-package-prices.php` - Plugin header comment (line 7) 2. `wc-tier-and-package-prices.php` - `WC_TPP_VERSION` constant (line 26) 3. `composer.json` - version field (optional, not critical) ### Creating Release Package **CRITICAL:** The zip command must be run from the **parent directory** of the plugin folder to create proper archive structure. ```bash # From parent directory (/home/magdev/workspaces/php) cd /home/magdev/workspaces/php # Create zip excluding dev files - note the correct path structure zip -r wc-tier-and-package-prices/releases/wc-tier-and-package-prices-X.X.X.zip wc-tier-and-package-prices/ \ -x 'wc-tier-and-package-prices/.git*' \ 'wc-tier-and-package-prices/*.log' \ 'wc-tier-and-package-prices/.claude/*' \ 'wc-tier-and-package-prices/CLAUDE.md' \ 'wc-tier-and-package-prices/releases/*' \ 'wc-tier-and-package-prices/node_modules/*' \ 'wc-tier-and-package-prices/.DS_Store' \ 'wc-tier-and-package-prices/Thumbs.db' \ 'wc-tier-and-package-prices/.vscode/*' \ 'wc-tier-and-package-prices/.idea/*' \ 'wc-tier-and-package-prices/*.sublime-*' \ 'wc-tier-and-package-prices/notes.*' \ 'wc-tier-and-package-prices/logs/*' \ 'wc-tier-and-package-prices/templates/cache/*' \ 'wc-tier-and-package-prices/composer.lock' # Return to project directory cd wc-tier-and-package-prices # Generate SHA256 checksum cd releases sha256sum wc-tier-and-package-prices-X.X.X.zip > wc-tier-and-package-prices-X.X.X.zip.sha256 cd .. ``` **IMPORTANT NOTES:** - The `vendor/` directory MUST be included in releases (Twig dependency required for runtime) - Running zip from wrong directory creates empty or malformed archives - Exclusion patterns must match the relative path structure used in zip command - Always verify the package with `unzip -l` and test extraction before committing ### Verification Steps After creating the release package, always verify: ```bash # Check package size (should be ~400-450KB, NOT 8MB+ or near 0) ls -lh releases/wc-tier-and-package-prices-X.X.X.zip # Verify exclusions worked unzip -l releases/wc-tier-and-package-prices-X.X.X.zip | grep -E "CLAUDE\.md|\.claude/|\.git" && echo "ERROR: Excluded files found!" || echo "OK: No excluded files" # Test extraction cd /tmp && rm -rf test-extract && unzip -q /path/to/releases/wc-tier-and-package-prices-X.X.X.zip -d test-extract && ls -la test-extract/wc-tier-and-package-prices/ # Verify version in extracted package head -30 /tmp/test-extract/wc-tier-and-package-prices/wc-tier-and-package-prices.php | grep -E "Version:|WC_TPP_VERSION" # Verify template changes (if applicable) grep 'class="regular"' /tmp/test-extract/wc-tier-and-package-prices/templates/admin/*.twig ``` ### Git Workflow for Releases **Standard workflow:** Work on `dev` branch → merge to `main` → tag → push ```bash # 1. Ensure you're on dev branch with all changes committed git checkout dev git add [files] git commit -m "Release version X.X.X - [description] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 " # 2. Merge dev to main git checkout main git merge dev --no-edit # Should be fast-forward # 3. Create annotated tag git tag -a vX.X.X -m "Release version X.X.X - [description]" # 4. Push everything git push origin main git push origin vX.X.X # 5. Update dev and push git checkout dev git rebase main # Should be up-to-date already git push origin dev # 6. If you have uncommitted local changes (like .claude/settings.local.json) git stash push -m "Local development settings" # ... do git operations ... git stash pop ``` **Important Git Notes:** - Always commit from `dev` branch first - Tags should use format `vX.X.X` (e.g., `v1.1.22`) - Use annotated tags (`-a`) not lightweight tags - Commit messages should follow the established format with Claude Code attribution - `.claude/settings.local.json` changes are typically local-only (stash before rebasing) ### What Gets Released - All plugin source files - Compiled vendor dependencies - Translation files (.mo compiled from .po) - Assets (CSS, JS) - Documentation (README, CHANGELOG, etc.) ### What's Excluded - Git metadata (`.git/`) - Development files (`.vscode/`, `.claude/`, `CLAUDE.md`) - Logs and cache files - Previous releases - `composer.lock` (but `vendor/` is included) ## Testing Checklist When making changes, test these critical paths: ### Admin - [ ] Settings page appears once under WooCommerce > Tier & Package Prices - [ ] Settings save correctly - [ ] Product edit page shows tier/package meta boxes - [ ] Adding/removing tiers works - [ ] Adding/removing packages works - [ ] Data saves when clicking "Update" on product ### Frontend (Product Page) - [ ] Pricing tables display when configured - [ ] Package buttons update quantity selector - [ ] Price updates dynamically when quantity changes - [ ] Restricted products hide quantity input - [ ] "View Options" appears on catalog for restricted products ### Cart & Checkout - [ ] Correct prices applied for tier pricing - [ ] Correct prices applied for package pricing - [ ] Cart displays pricing type labels - [ ] Package quantities can't be edited if restricted - [ ] Prices recalculate if quantity changed (non-restricted products) - [ ] Checkout totals are correct ### WooCommerce Blocks (Critical!) - [ ] Mini cart block doesn't throw fatal errors - [ ] Cart block works correctly - [ ] Checkout block processes orders - [ ] Quantity editable flag works for blocks ## Development Tips for Future AI Assistants ### Common Pitfalls and Solutions #### Release Package Creation **Problem:** Empty or corrupted zip files (0 bytes or wrong structure) **Cause:** Running zip command from wrong directory or incorrect path patterns **Solution:** Always run from parent directory (`/home/magdev/workspaces/node`) and use full relative paths in exclusions **Problem:** Development files included in release (CLAUDE.md, .claude/, .git) **Cause:** Exclusion patterns don't match actual paths used in zip command **Solution:** Test with `unzip -l | grep` to verify exclusions before committing **Problem:** Package size is 8MB+ instead of ~400KB **Cause:** Development files not excluded (especially .git directory) **Solution:** Follow verification steps and check package size immediately after creation #### UI Changes in Admin **WooCommerce CSS Classes:** - `short` - Small input fields (~60px width) - `regular` - Medium input fields (~120px width) - `long` - Large input fields (~200px+ width) When modifying admin input fields in Twig templates, use WooCommerce's standard classes for consistency. **Location:** `templates/admin/*.twig` for admin UI changes #### CSS Specificity and WooCommerce Overrides **CRITICAL LESSON from v1.2.4:** WooCommerce's core admin CSS uses high-specificity selectors that require `!important` to override. **Problem Symptoms:** - CSS rules not applying despite correct selectors - Styles work in simple cases but fail with WooCommerce elements - Browser DevTools shows rule crossed out or overridden **Diagnostic Steps:** 1. Inspect element in browser DevTools 2. Check "Computed" tab to see which styles are actually applied 3. Look for crossed-out rules in "Styles" tab (indicates override) 4. Check selector specificity - WooCommerce often uses complex selectors **Solution Patterns:** **For Table Styling:** ```css /* ❌ This will likely be overridden */ .wc-tpp-tiers-table { border: none; } /* ✅ Use !important for core WooCommerce overrides */ .wc-tpp-tiers-table, .wc-tpp-tiers-table th, .wc-tpp-tiers-table td, .wc-tpp-tiers-table thead, .wc-tpp-tiers-table tbody, .wc-tpp-tiers-table tr { border: none !important; } /* ✅ Also use border-collapse to prevent cell borders */ .wc-tpp-tiers-table { border-collapse: collapse !important; } ``` **For Float-Based Layouts:** ```css /* ❌ Float positioning is hard to control precisely */ .woocommerce-help-tip { float: right; margin-left: 10px; } /* ✅ Use flexbox for precise control */ label[for="_wc_tpp_restrict_to_packages"] { display: inline-flex; align-items: center; gap: 0; } .woocommerce-help-tip { float: none; display: inline-block; vertical-align: middle; margin-left: 6px; margin-right: 0; } ``` **General Rules:** 1. **Always test in actual WordPress admin** - browser preview may not show WooCommerce's CSS 2. **Target all related elements** - tables require styling on `table`, `thead`, `tbody`, `tr`, `th`, `td` 3. **Use `!important` sparingly but don't fear it** - sometimes it's the only way to override WooCommerce core 4. **Prefer flexbox over floats** - gives better control over alignment and spacing 5. **Check across browsers** - table rendering can vary between Chrome/Firefox/Safari **When Styles Don't Apply:** - First verify selector is correct (DevTools should show rule, even if crossed out) - If selector is correct but crossed out, increase specificity or add `!important` - If selector doesn't appear at all, check file is enqueued and cache is cleared - Use browser's "Inspect" right-click to see exact element structure #### Git Workflow Issues **Problem:** Cannot rebase due to uncommitted changes **Solution:** Stash local config files (`.claude/settings.local.json`) before git operations **Problem:** Tag already exists **Solution:** Delete with `git tag -d vX.X.X` locally and `git push --delete origin vX.X.X` remotely **Problem:** Wrong branch for commits **Solution:** Always start on `dev` branch, merge to `main`, never commit directly to `main` ### Working with Twig Templates The plugin uses Twig 3.0 for templating. Key files: - `templates/admin/tier-row.twig` - Single tier input row in product edit page - `templates/admin/package-row.twig` - Single package input row in product edit page - `templates/frontend/*.twig` - Customer-facing pricing displays **Template rendering:** Done via `WC_TPP_Template_Loader` singleton class **When modifying templates:** 1. Templates are cached - clear cache or test in development mode 2. Always escape output: use Twig's built-in filters or `|esc_attr`, `|esc_html` 3. Translation strings: `{{ 'Text'|__('wc-tier-package-prices') }}` 4. Keep consistent with WooCommerce admin UI patterns ### Complete Release Workflow Summary Based on v1.1.22, v1.2.2, and v1.2.3 release experience, here's the complete workflow: 1. **Fix bugs/add features** on `dev` branch 2. **Update version numbers** (3 files: main plugin file header, constant, composer.json) 3. **Update CHANGELOG.md** with detailed changes 4. **Update CLAUDE.md** version number and roadmap 5. **Create release package** from parent directory with correct exclusions 6. **Verify package** (size, contents, exclusions, extraction test) 7. **Commit changes** to `dev` branch with proper message format 8. **Merge to main** (fast-forward merge) 9. **Create annotated tag** (`vX.X.X`) 10. **Push all** (main, tag, dev) 11. **Verify remote** (check repository web UI) **Time estimate:** 15-20 minutes for full release cycle **Files typically changed in a release:** - `wc-tier-and-package-prices.php` - Version bumps - `composer.json` - Version bump - `CHANGELOG.md` - Release notes - `CLAUDE.md` - Version and roadmap updates - Feature-specific files (templates, PHP classes, etc.) **Note:** Release packages (`releases/`) are not tracked in git - they are generated locally for distribution. ### Release Package Creation - Critical Notes **IMPORTANT:** The zip command must be run from the **parent directory** to create proper archive structure. **Correct command (from `/home/magdev/workspaces/php`):** ```bash cd /home/magdev/workspaces/php zip -r wc-tier-and-package-prices/releases/wc-tier-and-package-prices-X.X.X.zip wc-tier-and-package-prices/ \ -x '*/\.git/*' '*/.git/*' 'wc-tier-and-package-prices/.git/*' \ '*.gitignore' '*.log' '*/.claude/*' '*/CLAUDE.md' \ '*/releases/*' '*/wordpress/*' '*/node_modules/*' \ '*/.DS_Store' '*/Thumbs.db' '*/.vscode/*' '*/.idea/*' \ '*.sublime-*' '*/notes.*' '*/logs/*' '*/templates/cache/*' \ '*/composer.lock' ``` **Critical Exclusions:** - `*/wordpress/*` and `*/core/*` - MUST be excluded! The project has a symlink to WordPress installation that zip will follow, creating 129MB+ packages instead of ~430KB - `.git/*` - All git metadata (multiple patterns needed for reliability) - `.claude/*` and `CLAUDE.md` - Development documentation - `releases/*` - Prevents including previous releases in new ones - `composer.lock` - Not needed in production (vendor/ is included) **Expected Package Size:** ~430-431KB (383 files) **Package Size Alert:** If >1MB, exclusions failed (likely wordpress symlink included) **Verification Steps:** ```bash # 1. Check size (should be ~430KB) ls -lh releases/wc-tier-and-package-prices-X.X.X.zip # 2. Verify file count (should be 383 files) unzip -l releases/wc-tier-and-package-prices-X.X.X.zip | tail -1 # 3. Check for excluded files unzip -l releases/wc-tier-and-package-prices-X.X.X.zip | grep -E "CLAUDE\.md|\.claude/|\.git/|wordpress/" # Should return nothing (exit code 1) # 4. Verify version in package unzip -p releases/wc-tier-and-package-prices-X.X.X.zip wc-tier-and-package-prices/wc-tier-and-package-prices.php | head -30 | grep -E "Version:|WC_TPP_VERSION" ``` ### Future Features and Roadmap The is a hierarchical list for upcoming features and can be considered as a Roadmap for the upcoming development. #### Version 1.1.x (Completed) 1. ~~Add translations for `de_CH`, `de_DE_informal`, `fr_CH`, `it_CH`~~ ✅ **COMPLETED in v1.1.21** 2. ~~The double-install bug is back again. A new version of the plugin is installed as new plugin instead of updating the previous version.~~ ✅ **DOCUMENTED in v1.1.22** - Added workaround to CHANGELOG. Root cause: No automatic update mechanism (requires WordPress.org repository or custom update server). 3. ~~Make the label fields in the backend for tierprices and package-prices twice as long as it is.~~ ✅ **COMPLETED in v1.1.22** 4. ~~Make the plugin work with variable products~~ ✅ **COMPLETED in v1.2.0** - Full variation-level pricing support with independent configuration per variation, AJAX-based frontend display, and complete WooCommerce Blocks compatibility. #### Version 1.2.x ##### Bugfixes (Completed in v1.2.1) 1. ~~The admin templates are not show right. The row templates didn't match the new table structure. The table-body columns didn't fit the table-head columns.~~ ✅ **FIXED in v1.2.1** - Updated admin.css to remove flexbox styling that was breaking the new `// // ❌ Wrong - Hard-coded or missing currency ``` **Twig Template Placeholders:** ```twig {# ✅ Correct - Pass currency_symbol from PHP and concatenate (CORRECTED in v1.2.9 - no translation filter) #} placeholder="{{ 'e.g., 9.99 ' ~ currency_symbol }}" {# ❌ Wrong - Hard-coded or missing currency #} placeholder="{{ 'e.g., 9.99'|__('wc-tier-package-prices') }}" placeholder="{{ 'e.g., 9.99 €'|__('wc-tier-package-prices') }}" ``` **Implementation Pattern:** 1. In PHP render methods, pass currency symbol to Twig: `'currency_symbol' => get_woocommerce_currency_symbol()` 2. In Twig templates, concatenate using `~` operator: `'text ' ~ currency_symbol` 3. Always use WooCommerce's `get_woocommerce_currency_symbol()` - never hard-code currency symbols **Affected Methods:** All template render methods must pass `currency_symbol`: - `render_tier_row()` - `render_package_row()` - `render_variation_tier_row()` - `render_variation_package_row()` #### CRITICAL: Post Meta Deletion vs. Empty Arrays (Learned in v1.2.8) When saving product meta data, WordPress distinguishes between "no data" (deleted meta) and "empty data" (empty array saved as meta): **Problem Pattern:** ```php // ❌ WRONG - Saves empty array when all entries removed if (isset($_POST['_wc_tpp_tiers'])) { $tiers = array(); foreach ($_POST['_wc_tpp_tiers'] as $tier) { if (!empty($tier['min_qty']) && !empty($tier['price'])) { $tiers[] = array(...); } } update_post_meta($post_id, '_wc_tpp_tiers', $tiers); // Saves [] if no valid entries } else { delete_post_meta($post_id, '_wc_tpp_tiers'); } ``` **Correct Pattern:** ```php // ✅ CORRECT - Deletes meta when no valid entries exist if (isset($_POST['_wc_tpp_tiers'])) { $tiers = array(); foreach ($_POST['_wc_tpp_tiers'] as $tier) { if (!empty($tier['min_qty']) && !empty($tier['price'])) { $tiers[] = array(...); } } // Only save if we have valid entries, otherwise delete if (!empty($tiers)) { update_post_meta($post_id, '_wc_tpp_tiers', $tiers); } else { delete_post_meta($post_id, '_wc_tpp_tiers'); } } else { delete_post_meta($post_id, '_wc_tpp_tiers'); } ``` **Why This Matters:** - Empty arrays `[]` saved via `update_post_meta()` persist in database as serialized empty arrays - Frontend/cart code checking `if ($tiers)` will evaluate `[]` as falsy, but meta still exists in database - Database queries like `get_post_meta()` return `[]` instead of `false`, causing subtle bugs - Properly deleting meta keeps database clean and prevents "ghost" configurations **Affected Methods in v1.2.8:** - `save_tier_package_fields()` - Simple and variable parent products - `save_variation_pricing_fields()` - Individual variations **Rule:** Always check `if (!empty($array))` before calling `update_post_meta()` for array data. If empty, call `delete_post_meta()` instead. #### CRITICAL: WordPress Translation Functions with printf (Learned in v1.2.9) When using `printf()` with WordPress translation functions, the text domain must be passed to the translation function, NOT to printf: **Wrong Pattern:** ```php // ❌ WRONG - Text domain not passed to translation function printf(__('Price (%s)', 'wc-tier-package-prices'), get_woocommerce_currency_symbol()); ``` **Problem:** The `__()` function receives the text domain as a second parameter, but in this pattern it's missing. This causes the string "Price (%s)" to not be found in translation files, so it won't be translated. **Correct Pattern:** ```php // ✅ CORRECT - Text domain in translation function, with esc_html for output escaping printf(esc_html__('Price (%s)', 'wc-tier-package-prices'), get_woocommerce_currency_symbol()); ``` **Why This Works:** - `esc_html__('Price (%s)', 'wc-tier-package-prices')` translates the string and returns it - `printf()` then substitutes the `%s` placeholder with the currency symbol - The translated string is used in the final output - `esc_html` ensures proper output escaping **Applied in v1.2.9:** All 6 table headers in `includes/class-wc-tpp-product-meta.php` #### CRITICAL: Twig Translation Filters and HTML Entity Encoding (Learned in v1.2.9) When concatenating dynamic values in Twig templates, applying the translation filter can cause HTML entity encoding issues: **Wrong Pattern:** ```twig {# ❌ WRONG - Translation filter encodes special characters in concatenated string #} placeholder="{{ ('e.g., 9.99 ' ~ currency_symbol)|__('wc-tier-package-prices') }}" ``` **Problem:** When `currency_symbol` contains special characters (€, £, ¥, etc.), the concatenated string is passed through the translation function which treats it as a translatable string and encodes special characters as HTML entities (`€`, `£`, etc.). **Correct Pattern:** ```twig {# ✅ CORRECT - No translation filter, just concatenation #} placeholder="{{ 'e.g., 9.99 ' ~ currency_symbol }}" ``` **Why This Works:** - Placeholder examples don't need translation - they're illustrative values - Direct concatenation preserves special characters - Currency symbol displays correctly (€ instead of €) **Rule:** Only apply translation filters to static text that needs translation, not to concatenated strings with dynamic values that contain special characters. **Applied in v1.2.9:** - `templates/admin/tier-row.twig` - Price input placeholder - `templates/admin/package-row.twig` - Price input placeholder #### CRITICAL: Defensive Programming for POST Data Processing (Learned in v1.2.9) The v1.2.8 fix for variation pricing deletion had the right logic but used a branching structure that could miss edge cases. The v1.2.9 refactor demonstrates a more defensive pattern: **Less Defensive Pattern (v1.2.8):** ```php // ❌ BRITTLE - Multiple branches, easy to miss edge cases if (isset($_POST['wc_tpp_tiers'][$loop])) { $tiers = array(); foreach ($_POST['wc_tpp_tiers'][$loop] as $tier) { // ... populate tiers ... } if (!empty($tiers)) { update_post_meta($variation_id, '_wc_tpp_tiers', $tiers); } else { delete_post_meta($variation_id, '_wc_tpp_tiers'); } } else { delete_post_meta($variation_id, '_wc_tpp_tiers'); } ``` **Problem:** Two separate code paths to `delete_post_meta()`. If logic changes, easy to update one path but forget the other. **More Defensive Pattern (v1.2.9):** ```php // ✅ DEFENSIVE - Single decision point, guaranteed cleanup $tiers = array(); if (isset($_POST['wc_tpp_tiers'][$loop]) && is_array($_POST['wc_tpp_tiers'][$loop])) { foreach ($_POST['wc_tpp_tiers'][$loop] as $tier) { // ... populate tiers ... } } // Always execute one of these based on final state if (!empty($tiers)) { update_post_meta($variation_id, '_wc_tpp_tiers', $tiers); } else { delete_post_meta($variation_id, '_wc_tpp_tiers'); } ``` **Why This Is Better:** - Initialize array at the start (guaranteed initial state) - Single conditional for populating (with extra `is_array()` safety check) - Single decision point for save/delete (one place to maintain) - Impossible to have a code path that doesn't call either update or delete - Much easier to reason about and modify **Key Principles:** 1. **Initialize variables early** - Establish known initial state 2. **Minimize branching** - Fewer code paths = fewer bugs 3. **Single decision point** - One place determines final action 4. **Add safety checks** - Validate assumptions (`is_array()`) 5. **Guaranteed execution** - Always perform one of update/delete, never neither **Applied in v1.2.9:** - `save_variation_pricing_fields()` - Both tier and package pricing logic refactored **Rule:** When processing user input to decide between update and delete, prefer the pattern: initialize → conditionally populate → unconditionally act based on final state. ### When Adding Features - Follow the existing pattern: add setting → add UI → add logic → add template - Use Twig for all new templates (consistency) - Add translations for all user-facing strings - Test with both simple products and variable products (if applicable) - Consider both classic and block-based cart/checkout ### When Fixing Bugs 1. Check `CHANGELOG.md` for historical context 2. Look for similar issues in past versions 3. Always add detailed changelog entry explaining root cause 4. Consider edge cases (guest checkout, logged-in users, AJAX add-to-cart, etc.) ## File Locations Quick Reference | Task | File(s) | | ------ | --------- | | Change version | `wc-tier-and-package-prices.php` (2 places) | | Add global setting | `includes/class-wc-tpp-settings.php` | | Modify product meta box | `includes/class-wc-tpp-product-meta.php` + `templates/admin/*.twig` | | Change product page display | `includes/class-wc-tpp-frontend.php` + `templates/frontend/*.twig` | | Fix cart pricing | `includes/class-wc-tpp-cart.php` | | Update styles | `assets/css/frontend.css` or `assets/css/admin.css` | | Fix JavaScript bugs | `assets/js/frontend.js` or `assets/js/admin.js` | | Add translations | `languages/*.po` then compile to `.mo` | | Document changes | `CHANGELOG.md` | ## Compatibility Notes ### WordPress - Minimum: 6.0 - Tested up to: 6.9.x - Uses standard plugin API, no deprecated functions ### WooCommerce - Minimum: 8.0 - Tested up to: 10.x - HPOS compatible (declared via `FeaturesUtil::declare_compatibility`) - Blocks compatible (with proper filter handling) ### PHP - Minimum: 8.3 (breaking change in v1.3.0) - Uses modern PHP features (type hints, named arguments, etc.) - Composer autoloader handles namespacing ### Browsers - Modern browsers (ES6+ JavaScript) - Responsive CSS (mobile-friendly) - jQuery dependency (WooCommerce provides) ## Support & Resources - **Repository:** - **Documentation:** See `README.md`, `QUICKSTART.md`, `USAGE_EXAMPLES.md`, `INSTALLATION.md` - **Changelog:** `CHANGELOG.md` (detailed version history) - **Issue Tracking:** Check fatal-errors-*.log files for production errors ## Final Notes This is a production-quality plugin with real-world usage. Any changes should: 1. Maintain backward compatibility with existing tier/package configurations 2. Not break WooCommerce core functionality 3. Work with both classic and block-based themes 4. Be thoroughly tested before release 5. Include proper error handling and validation 6. Update CHANGELOG.md with detailed explanations The plugin architecture is solid and well-tested. Most bugs arise from: - WooCommerce API changes (especially blocks) - Filter/action signature changes - Edge cases in cart calculations - Settings persistence issues --- ## Session History ### v1.3.0 Release Session (2026-01-25) **Accomplished:** 1. Fixed known bugs from CLAUDE.md: - Removed all releases from git tracking (`git rm --cached -r releases/`) - Deleted all MD5 checksum files, keeping only SHA256 - Updated `.gitignore` to exclude `/releases/` 2. Implemented v1.3.0 features: - Bumped PHP requirement from 7.4 to 8.3 (breaking change) - Added PHP version check with admin notice for incompatible servers - Added `magdev/wc-licensed-product-client` library integration - Refactored settings page to use WooCommerce modern sub-tabs pattern (`get_own_sections()`) - Created "General" and "License" sub-tabs - Implemented license management with AJAX validation/activation - Added license status caching via WordPress transients - Added CSS styling for license status display 3. Updated all translation files: - Added 28 new translation strings for license management - Updated .pot template and all 7 .po files - Compiled all .mo files 4. Created release package: - Package size: 737KB (increased due to new dependencies) - 642 files included (more due to license client library) - SHA256 checksum generated **Key Learnings:** - WooCommerce settings sub-tabs use `get_own_sections()` and `get_settings_for_{section}_section()` pattern - License client library `magdev/wc-licensed-product-client` is from private Gitea repo - requires `repositories` config in composer.json - Package version was `^0.1` not `^1.0` - always check available versions before setting constraint - Release package size increased from ~430KB to ~737KB due to new Composer dependencies ### v1.3.1 Release Session (2026-01-27) **Accomplished:** 1. Reviewed server-implementation.md from license client library 2. Upgraded from `LicenseClient` to `SecureLicenseClient`: - Added "Server Secret" configuration field in License settings tab - Updated `get_license_client()` to use `SecureLicenseClient` with HMAC-SHA256 signature verification - Updated JavaScript to pass `server_secret` in AJAX requests 3. Added comprehensive exception handling: - `RateLimitExceededException` - Shows retry time to user - `SignatureException` - Reports signature verification failures - `InvalidArgumentException` - SSRF protection for private IP blocking 4. Updated Composer dependencies: - Changed `magdev/wc-licensed-product-client` from `^0.1` to `dev-main` - Library includes new security features (SSRF protection, response signatures) 5. Updated translations: - Added 7 new translation strings for v1.3.1 - All 7 language files updated and compiled 6. Created release package: - Package size: 745KB - 642 files included - SHA256 checksum generated 7. Updated README.md: - Documented license management feature - Updated PHP requirement to 8.3 - Added v1.3.0 and v1.3.1 changelog entries **Key Learnings:** - `SecureLicenseClient` requires `serverSecret` parameter for HMAC verification - The library's exception classes are in `Magdev\WcLicensedProductClient\Exception\` and `Magdev\WcLicensedProductClient\Security\` namespaces - `RateLimitExceededException` has a `retryAfter` property (int seconds) - Always check library documentation in `docs/server-implementation.md` for implementation requirements --- Always refer to this document when starting work on this project. Good luck!
` structure introduced in v1.2.0. The CSS was still using flexbox layout from the old `
/

` structure. 2. ~~The tier and package prices are not shown on simple product pages~~ ✅ **FIXED in v1.2.1** - Removed global enable/disable checks from frontend template. Pricing tables now display if configured on a product AND the "Display Pricing Table" setting is enabled, regardless of "Enable Tier Pricing" or "Enable Package Pricing" global settings. Cart calculations still respect global enable settings. ##### Bugfixes (Completed in v1.2.2) 1. ~~Remove the table borders in admin on variable product to better fit the surrounding element styles.~~ ✅ **FIXED in v1.2.2** - Added CSS rules to remove table borders specifically for variation pricing tables (`.wc-tpp-variation-pricing`), matching WooCommerce's borderless variation UI style. 2. ~~Add missing translations in admin templates ("Price", "Tier & Package Pricing", "Min Quantity") for all languages used in this project.~~ ✅ **FIXED in v1.2.2** - Added missing translation entries for "Min Quantity", "Price", and "Label (optional)" to all .po files (de_DE, de_DE_informal, de_CH, de_CH_informal, fr_CH, it_CH, en_US) and recompiled .mo files. 3. ~~Check the template for wc_tpp_restrict_to_packages[] checkbox elements in admin on variable products and fix the rendering.~~ ✅ **FIXED in v1.2.2** - Fixed checkbox value parameter in variation pricing fields. Changed from ternary expression to direct value assignment, allowing WooCommerce's `woocommerce_wp_checkbox()` to properly handle the checked state. ##### Bugfixes (Completed in v1.2.3) 1. ~~Style the tier and packages tables in admin on simple products according to the styles on variable products.~~ ✅ **FIXED in v1.2.3** - Applied borderless table styling to all tier/package tables (both simple and variable products). Removed borders from table, th, and td elements to match WooCommerce's clean admin UI style. 2. ~~The checkbox styles from 1.2.2 bug 3 are still not looking correct. The helptext is written instead of hidden after the help icon and the margin between checkbox and label are to small.~~ ✅ **FIXED in v1.2.3** - Added `desc_tip => true` to variation checkbox to show tooltip instead of inline text. Added CSS rules to increase checkbox-label margin (8px) and hide inline description text when tooltip is used. ##### Bugfixes (Completed in v1.2.4) 1. ~~Bug 1 in v1.2.3 is not fixed. Now both table display have border again. they shouldn't have border.~~ ✅ **FIXED in v1.2.4** - Added `!important` flags and `border-collapse: collapse` to table CSS to override WooCommerce's default table styling. Added comprehensive border removal for all table elements (table, thead, tbody, tr, th, td) to ensure truly borderless tables across all browsers. 2. ~~Bug 2 in v1.2.3: Increase the margin between checkbox and label and put the help icon right next to the label, not at the right border~~ ✅ **FIXED in v1.2.4** - Increased checkbox right margin from 8px to 12px. Repositioned help tip icon to display inline right next to the label text using flexbox layout with `display: inline-flex`, removing float positioning that caused it to appear at the right edge. ##### Enhancements (Completed in v1.2.5) 1. ~~Hide the table-headers in admin area until a tier or respectivly a package price is defined.~~ ✅ **COMPLETED in v1.2.5** - Added CSS `:has()` pseudo-class selectors to automatically hide table headers when tbody is empty. Creates a cleaner interface showing only the helpful empty state message and "Add" button when no pricing rules are configured. 2. ~~Make it possible to define tier or package prices on variable products in the parent product as a default for that product and all variants of it unless a variant has its own tier or package prices.~~ ✅ **COMPLETED in v1.2.5** - Implemented parent product default pricing with automatic fallback. Variable products can define tier/package pricing once at parent level; variations inherit these defaults unless they have their own specific pricing. Added helper methods in cart class and updated all pricing/restriction checks to support parent fallback. ##### Bugfixes (Completed in v1.2.6 and v1.2.7) 1. ~~Table headers in admin are still visible when empty.~~ ✅ **FIXED in v1.2.7** - The CSS `:has()` pseudo-class approach from v1.2.5/v1.2.6 wasn't working reliably across all browsers. Implemented JavaScript-based solution that adds/removes `has-rows` class on tables. Headers now hide by default (CSS) and show only when table has rows (JavaScript toggles class). Function `updateTableHeaders()` is called on page load and after all add/remove row operations. 2. ~~Parent product pricing forms not visible in admin.~~ ✅ **FIXED in v1.2.6 and v1.2.7** - The backend fallback logic from v1.2.5 was implemented but the admin UI to configure it was missing. Added `add_variable_parent_pricing_fields()` method that displays pricing forms for variable product parents. Fixed hook issue in v1.2.7: changed from `woocommerce_product_options_pricing` (only fires for simple products) to `woocommerce_product_options_general_product_data` (fires for all product types). Variable product parents now have a "Default Tier & Package Pricing for All Variations" section where defaults can be configured. ##### Translation Updates (Completed in v1.2.7) 1. ✅ **COMPLETED** - Updated all translation files (.pot, .po, .mo) with new strings from v1.2.6 and v1.2.7 for variable product parent pricing features. All 7 language variants updated with translations for "Default Tier & Package Pricing for All Variations" and related strings. ##### Bugfixes (Completed in v1.2.8) 1. ~~Add a Suffix with the current configured default currency to the table-header and form placeholder. Use the common currency notation in placeholder~~ ✅ **FIXED in v1.2.8** - Updated all table headers in admin to display "Price (€)" format using `printf(__('Price (%s)'), get_woocommerce_currency_symbol())`. Modified all template render methods (tier_row, package_row, variation_tier_row, variation_package_row) to pass currency_symbol to Twig templates. Updated admin/tier-row.twig and admin/package-row.twig to concatenate currency symbol in price input placeholders (e.g., "e.g., 9.99 €"). Applied to simple products, variable parent products, and all variations. 2. ~~Already stored tier and package prices on the children of a variable product are still available after deletion. Looks like the storage mechanism has an error. This occurs only on the child product, not on the parent product.~~ ✅ **FIXED in v1.2.8** - Fixed save logic in both `save_tier_package_fields()` and `save_variation_pricing_fields()` methods. Root cause: Empty arrays were being saved via `update_post_meta()` instead of being deleted. Changed logic from "save on isset, delete otherwise" to "filter entries, then save if not empty, delete if empty". Added `if (!empty($tiers))` and `if (!empty($packages))` checks before calling `update_post_meta()`. Now properly calls `delete_post_meta()` when all pricing entries are removed, preventing empty arrays from persisting in database. ##### Bugfixes (Completed in v1.2.9) 1. ~~The Price header in admin tables while configuring tier and package prices is not translated. Also the placeholder on the form elements for prices has the wrong encoding, the special characters on the placeholder are show in html-entity encoding.~~ ✅ **FIXED in v1.2.9** - Fixed translation function placement in printf statements by changing from `printf(__('Price (%s)', ...), ...)` to `printf(esc_html__('Price (%s)', 'wc-tier-package-prices'), ...)`. This ensures the text domain is passed correctly to the translation function while maintaining the currency placeholder functionality. Also removed the translation filter from concatenated placeholder strings in Twig templates (changed from `{{ ('e.g., 9.99 ' ~ currency_symbol)|__('wc-tier-package-prices') }}` to `{{ 'e.g., 9.99 ' ~ currency_symbol }}`) because example values should not be translated and the filter was causing HTML entity encoding of special characters. 2. ~~The tier and package prices for children of a variable product are still not deletable. After storing the product, the previously deleted rows are back again.~~ ✅ **FIXED in v1.2.9** - Despite the fix in v1.2.8, edge cases remained due to conditional branching structure. Refactored `save_variation_pricing_fields()` with more defensive logic: initialize arrays at the start ($tiers = array()), populate only if valid POST data exists (with added is_array() check), then unconditionally perform either update_post_meta() (if !empty) or delete_post_meta() (if empty). This guarantees proper cleanup regardless of POST data structure and eliminates the if/else branching that could miss edge cases. ##### Planned Enhancements for v1.2.10+ 1. Create different, selectable templates for tierprices and packages to use in the frontend. Make the new templates selectable globally on the settings-page, not per product. ### When Debugging Cart Issues 1. Check `includes/class-wc-tpp-cart.php` first 2. The `apply_tier_package_pricing()` method runs on `woocommerce_before_calculate_totals` 3. Always validate product objects with `is_a($product, 'WC_Product')` 4. Remember: WooCommerce expects UNIT prices, not total prices (except for internal calculations) ### When Working with WooCommerce Hooks - WooCommerce has both classic and block-based systems - Classic cart uses different hooks than Store API (blocks) - Always check filter/action documentation for parameter types - Don't assume cart item arrays everywhere - sometimes it's product objects! #### CRITICAL: Product Type-Specific Hooks (Learned in v1.2.6/v1.2.7) WooCommerce has different hooks for different product types in the admin product edit page: - `woocommerce_product_options_pricing` - **ONLY fires for simple products**, NOT variable products - `woocommerce_product_options_general_product_data` - Fires for ALL product types after the general tab - `woocommerce_variation_options_pricing` - Fires for individual variations within variable products **Lesson:** When adding admin UI for variable product parents, use `woocommerce_product_options_general_product_data` and check `$product->is_type('variable')` to conditionally display. Using `woocommerce_product_options_pricing` will cause forms to never appear for variable products (as discovered in v1.2.6 → v1.2.7 fix). #### CRITICAL: Currency Symbol Display (Learned in v1.2.8, Corrected in v1.2.9) When displaying currency symbols in admin interface table headers and input placeholders: **Table Headers:** ```php // ✅ Correct - Use printf with esc_html__ for translation (CORRECTED in v1.2.9)