Files
wc-tier-and-package-prices/CLAUDE.md
magdev cbe758267e Document v1.3.0 session learnings in CLAUDE.md
- Updated PHP requirement from 7.4 to 8.3 in compatibility notes
- Added Session History section documenting v1.3.0 release session
- Documented key learnings about license client integration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:44:16 +01:00

51 KiB

WooCommerce Tier and Package Prices - AI Context Document

Last Updated: 2026-01-25 Current Version: 1.3.0 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.1

  • TBD

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

{
  "twig/twig": "^3.0",
  "magdev/wc-licensed-product-client": "^0.1",
  "symfony/http-client": "^7.0",
  "psr/log": "^3.0",
  "psr/cache": "^3.0",
  "psr/http-client": "^1.0"
}

Architecture

Directory Structure

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):

// 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):

// 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:

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:

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:

__('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)

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

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

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.

# 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:

# 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

# 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 <noreply@anthropic.com>"

# 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:

/* ❌ 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:

/* ❌ 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):

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:

# 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 <table>/<tr>/<td> structure introduced in v1.2.0. The CSS was still using flexbox layout from the old <div>/<p> 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:

// ✅ Correct - Use printf with esc_html__ for translation (CORRECTED in v1.2.9)
<th><?php printf(esc_html__('Price (%s)', 'wc-tier-package-prices'), get_woocommerce_currency_symbol()); ?></th>

// ❌ Wrong - Hard-coded or missing currency
<th><?php _e('Price', 'wc-tier-package-prices'); ?></th>
<th><?php _e('Price (€)', 'wc-tier-package-prices'); ?></th>

Twig Template Placeholders:

{# ✅ 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:

// ❌ 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:

// ✅ 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:

// ❌ 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:

// ✅ 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:

{# ❌ 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 (&euro;, &pound;, etc.).

Correct Pattern:

{# ✅ 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):

// ❌ 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):

// ✅ 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

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

Always refer to this document when starting work on this project. Good luck!