From e4ecc2c0bed49dd1b74862ae4beee4198509a03c Mon Sep 17 00:00:00 2001 From: magdev Date: Tue, 30 Dec 2025 00:23:56 +0100 Subject: [PATCH] Release version 1.2.5 - Parent product default pricing and UI enhancements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added: - Parent product default pricing for variable products - set tier/package pricing once on parent, variations inherit unless overridden - Hide empty table headers in admin until pricing rules are defined Technical: - Added parent fallback logic to get_tier_price() and get_package_price() methods - Created helper methods get_packages_with_fallback() and is_restriction_enabled() in cart class - Updated all cart methods to support parent product defaults - Added CSS :has() selectors to hide table headers when tbody is empty - Fixed cart pricing calls to pass correct product ID for fallback resolution 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- CHANGELOG.md | 31 +++++++++ CLAUDE.md | 6 +- assets/css/admin.css | 12 ++++ composer.json | 2 +- includes/class-wc-tpp-cart.php | 106 +++++++++++++++++------------ includes/class-wc-tpp-frontend.php | 10 +++ wc-tier-and-package-prices.php | 4 +- 7 files changed, 120 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be6baf2..8d68463 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,37 @@ All notable changes to WooCommerce Tier and Package Prices will be documented in The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.5] - 2025-12-30 + +### Added + +- **Parent Product Default Pricing**: Variable products can now define tier and package pricing at the parent product level that applies as defaults to all variations. Individual variations can override these defaults with their own specific pricing. This makes it much easier to set up pricing for products with many variations - set defaults once on the parent, then only customize the variations that need different pricing. + +- **Hide Empty Table Headers**: Table headers for tier and package pricing in the admin area now automatically hide when no pricing rules are defined. This creates a cleaner interface when starting to configure a product, showing only the helpful empty state message and "Add" button. + +### Technical Details + +**Parent Fallback Implementation**: +- Modified `WC_TPP_Frontend::get_tier_price()` and `WC_TPP_Frontend::get_package_price()` to fall back to parent product pricing when variation doesn't have its own pricing +- Updated `WC_TPP_Cart` to use helper methods `get_packages_with_fallback()` and `is_restriction_enabled()` for consistent parent fallback behavior +- All cart validation, quantity restriction, and display methods now support parent product defaults +- Fixed cart pricing calls to pass parent `$product_id` instead of `$effective_id` for proper fallback resolution + +**CSS Enhancement**: +- Added `:has()` pseudo-class selectors to hide table headers when tbody is empty +- Leverages existing empty state message styling for consistent UX + +**Backward Compatibility**: +- 100% backward compatible - existing products continue working as before +- No database migrations required +- Variations with specific pricing take precedence over parent defaults + +### Changed Files + +- `includes/class-wc-tpp-frontend.php` - Added parent fallback logic to `get_tier_price()` and `get_package_price()` methods +- `includes/class-wc-tpp-cart.php` - Added helper methods `get_packages_with_fallback()` and `is_restriction_enabled()`; updated all cart methods to support parent fallback; fixed pricing calls to use correct product ID +- `assets/css/admin.css` - Added CSS rules to hide table headers when no pricing rules exist + ## [1.2.4] - 2025-12-30 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 414ee4b..ed2a6e9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # WooCommerce Tier and Package Prices - AI Context Document **Last Updated:** 2025-12-30 -**Current Version:** 1.2.4 +**Current Version:** 1.2.5 **Author:** Marco Graetsch **Project Status:** Production-ready WordPress plugin @@ -765,10 +765,6 @@ Roadmap for the upcoming development. 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. -##### New Features - -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 diff --git a/assets/css/admin.css b/assets/css/admin.css index 8e3e351..60b4dd0 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -111,6 +111,18 @@ font-style: italic; } +/* Hide table headers when there are no pricing rules */ +.wc-tpp-tiers-container:empty ~ thead, +.wc-tpp-packages-container:empty ~ thead { + display: none; +} + +/* Alternative approach: hide thead when tbody is empty (more reliable) */ +.wc-tpp-tiers-table:has(tbody.wc-tpp-tiers-container:empty) thead, +.wc-tpp-packages-table:has(tbody.wc-tpp-packages-container:empty) thead { + display: none; +} + /* Checkbox styling improvements */ #_wc_tpp_restrict_to_packages, input[id^="wc_tpp_restrict_to_packages_"] { diff --git a/composer.json b/composer.json index 019aad6..56a2dea 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "magdev/wc-tier-package-prices", "description": "WooCommerce plugin for tier pricing and package prices with Twig templates", - "version": "1.2.4", + "version": "1.2.5", "type": "wordpress-plugin", "license": "GPL-2.0-or-later", "authors": [ diff --git a/includes/class-wc-tpp-cart.php b/includes/class-wc-tpp-cart.php index 2ca95fc..bcb8863 100644 --- a/includes/class-wc-tpp-cart.php +++ b/includes/class-wc-tpp-cart.php @@ -50,10 +50,10 @@ if (!class_exists('WC_TPP_Cart')) { continue; } - // Check for exact package match first (use effective ID for variations) + // Check for exact package match first (pass product_id for parent fallback support) $package_price = null; if (get_option('wc_tpp_enable_package_pricing') === 'yes') { - $package_price = WC_TPP_Frontend::get_package_price($effective_id, $quantity, $variation_id); + $package_price = WC_TPP_Frontend::get_package_price($product_id, $quantity, $variation_id); } if ($package_price !== null) { @@ -64,9 +64,9 @@ if (!class_exists('WC_TPP_Cart')) { WC()->cart->cart_contents[$cart_item_key]['wc_tpp_pricing_type'] = 'package'; WC()->cart->cart_contents[$cart_item_key]['wc_tpp_total_price'] = $package_price; } else { - // Apply tier pricing if no package match (use effective ID for variations) + // Apply tier pricing if no package match (pass product_id for parent fallback support) if (get_option('wc_tpp_enable_tier_pricing') === 'yes') { - $tier_price = WC_TPP_Frontend::get_tier_price($effective_id, $quantity, $variation_id); + $tier_price = WC_TPP_Frontend::get_tier_price($product_id, $quantity, $variation_id); if ($tier_price !== null) { $product->set_price($tier_price); // Store pricing information in cart item for display @@ -103,20 +103,16 @@ if (!class_exists('WC_TPP_Cart')) { public function validate_package_quantity($passed, $product_id, $quantity) { // Check for variation ID in request (for variable products) $variation_id = isset($_REQUEST['variation_id']) ? absint($_REQUEST['variation_id']) : 0; - $effective_id = $variation_id > 0 ? $variation_id : $product_id; - // Check if restriction is enabled globally or for this product/variation - $global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes'; - $product_restrict = get_post_meta($effective_id, '_wc_tpp_restrict_to_packages', true) === 'yes'; - - if (!$global_restrict && !$product_restrict) { + // Check if restriction is enabled (with parent fallback for variations) + if (!$this->is_restriction_enabled($product_id, $variation_id)) { return $passed; } - // Get packages for this product/variation - $packages = get_post_meta($effective_id, '_wc_tpp_packages', true); + // Get packages for this product/variation (with parent fallback) + $packages = $this->get_packages_with_fallback($product_id, $variation_id); - if (empty($packages) || !is_array($packages)) { + if (!$packages) { return $passed; } @@ -156,15 +152,8 @@ if (!class_exists('WC_TPP_Cart')) { $variation_id = isset($cart_item['variation_id']) ? absint($cart_item['variation_id']) : 0; $effective_id = $variation_id > 0 ? $variation_id : $product_id; - // Check if restriction is enabled globally or for this product/variation - $global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes'; - $product_restrict = get_post_meta($effective_id, '_wc_tpp_restrict_to_packages', true) === 'yes'; - - // Get packages for this product/variation - $packages = get_post_meta($effective_id, '_wc_tpp_packages', true); - - // If restriction is enabled and packages exist, show quantity as text only - if (($global_restrict || $product_restrict) && !empty($packages)) { + // Check if restriction is enabled (with parent fallback) and packages exist + if ($this->is_restriction_enabled($product_id, $variation_id) && $this->get_packages_with_fallback($product_id, $variation_id)) { return sprintf('%s', $effective_id, $cart_item['quantity'] @@ -179,15 +168,8 @@ if (!class_exists('WC_TPP_Cart')) { $variation_id = isset($cart_item['variation_id']) ? absint($cart_item['variation_id']) : 0; $effective_id = $variation_id > 0 ? $variation_id : $product_id; - // Check if restriction is enabled globally or for this product/variation - $global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes'; - $product_restrict = get_post_meta($effective_id, '_wc_tpp_restrict_to_packages', true) === 'yes'; - - // Get packages for this product/variation - $packages = get_post_meta($effective_id, '_wc_tpp_packages', true); - - // If restriction is enabled and packages exist, show quantity as text only - if (($global_restrict || $product_restrict) && !empty($packages)) { + // Check if restriction is enabled (with parent fallback) and packages exist + if ($this->is_restriction_enabled($product_id, $variation_id) && $this->get_packages_with_fallback($product_id, $variation_id)) { return sprintf('%s ×', $effective_id, $cart_item['quantity'] @@ -209,11 +191,8 @@ if (!class_exists('WC_TPP_Cart')) { $variation_id = isset($cart_item['variation_id']) ? absint($cart_item['variation_id']) : 0; $effective_id = $variation_id > 0 ? $variation_id : $product_id; - $global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes'; - $product_restrict = get_post_meta($effective_id, '_wc_tpp_restrict_to_packages', true) === 'yes'; - $packages = get_post_meta($effective_id, '_wc_tpp_packages', true); - - if (($global_restrict || $product_restrict) && !empty($packages)) { + // Check if restriction is enabled (with parent fallback) and packages exist + if ($this->is_restriction_enabled($product_id, $variation_id) && $this->get_packages_with_fallback($product_id, $variation_id)) { $restricted_products[] = $effective_id; } } @@ -254,20 +233,61 @@ if (!class_exists('WC_TPP_Cart')) { return $editable; } - // For variations, use the variation ID directly (get_id() returns variation ID for WC_Product_Variation) - $effective_id = $product_id; + // For variations, get parent product ID and variation ID + $variation_id = 0; + $parent_id = $product_id; - $global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes'; - $product_restrict = get_post_meta($effective_id, '_wc_tpp_restrict_to_packages', true) === 'yes'; - $packages = get_post_meta($effective_id, '_wc_tpp_packages', true); + if ($product->is_type('variation')) { + $variation_id = $product_id; + $parent_id = $product->get_parent_id(); + } - // If restriction is enabled and packages exist, make quantity non-editable - if (($global_restrict || $product_restrict) && !empty($packages)) { + // Check if restriction is enabled (with parent fallback) and packages exist + if ($this->is_restriction_enabled($parent_id, $variation_id) && $this->get_packages_with_fallback($parent_id, $variation_id)) { return false; } return $editable; } + + /** + * Get packages with parent fallback for variations + * + * @param int $product_id Parent product ID + * @param int $variation_id Variation ID (0 for simple products) + * @return array|false Packages array or false if none found + */ + private function get_packages_with_fallback($product_id, $variation_id = 0) { + $effective_id = $variation_id > 0 ? $variation_id : $product_id; + $packages = get_post_meta($effective_id, '_wc_tpp_packages', true); + + // Fall back to parent pricing if variation doesn't have its own pricing + if ((empty($packages) || !is_array($packages)) && $variation_id > 0) { + $packages = get_post_meta($product_id, '_wc_tpp_packages', true); + } + + return (!empty($packages) && is_array($packages)) ? $packages : false; + } + + /** + * Check if restriction is enabled for a product/variation with parent fallback + * + * @param int $product_id Parent product ID + * @param int $variation_id Variation ID (0 for simple products) + * @return bool Whether restriction is enabled + */ + private function is_restriction_enabled($product_id, $variation_id = 0) { + $effective_id = $variation_id > 0 ? $variation_id : $product_id; + $global_restrict = get_option('wc_tpp_restrict_package_quantities', 'no') === 'yes'; + $product_restrict = get_post_meta($effective_id, '_wc_tpp_restrict_to_packages', true) === 'yes'; + + // Fall back to parent restriction setting if variation doesn't have its own + if (!$product_restrict && $variation_id > 0) { + $product_restrict = get_post_meta($product_id, '_wc_tpp_restrict_to_packages', true) === 'yes'; + } + + return $global_restrict || $product_restrict; + } } new WC_TPP_Cart(); diff --git a/includes/class-wc-tpp-frontend.php b/includes/class-wc-tpp-frontend.php index 5dc6cad..9f412a0 100644 --- a/includes/class-wc-tpp-frontend.php +++ b/includes/class-wc-tpp-frontend.php @@ -133,6 +133,11 @@ if (!class_exists('WC_TPP_Frontend')) { $effective_id = $variation_id > 0 ? $variation_id : $product_id; $tiers = get_post_meta($effective_id, '_wc_tpp_tiers', true); + // Fall back to parent pricing if variation doesn't have its own pricing + if ((empty($tiers) || !is_array($tiers)) && $variation_id > 0) { + $tiers = get_post_meta($product_id, '_wc_tpp_tiers', true); + } + if (empty($tiers) || !is_array($tiers)) { return null; } @@ -159,6 +164,11 @@ if (!class_exists('WC_TPP_Frontend')) { $effective_id = $variation_id > 0 ? $variation_id : $product_id; $packages = get_post_meta($effective_id, '_wc_tpp_packages', true); + // Fall back to parent pricing if variation doesn't have its own pricing + if ((empty($packages) || !is_array($packages)) && $variation_id > 0) { + $packages = get_post_meta($product_id, '_wc_tpp_packages', true); + } + if (empty($packages) || !is_array($packages)) { return null; } diff --git a/wc-tier-and-package-prices.php b/wc-tier-and-package-prices.php index 14b8c37..a21a6b2 100644 --- a/wc-tier-and-package-prices.php +++ b/wc-tier-and-package-prices.php @@ -4,7 +4,7 @@ * Plugin Name: WooCommerce Tier and Package Prices * Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-tier-package-prices * Description: Add tier pricing and package prices to WooCommerce products with configurable quantities at fixed prices - * Version: 1.2.4 + * Version: 1.2.5 * Author: Marco Graetsch * Author URI: https://src.bundespruefstelle.ch/magdev * Text Domain: wc-tier-package-prices @@ -23,7 +23,7 @@ if (!defined('ABSPATH')) { // Define plugin constants if (!defined('WC_TPP_VERSION')) { - define('WC_TPP_VERSION', '1.2.4'); + define('WC_TPP_VERSION', '1.2.5'); } if (!defined('WC_TPP_PLUGIN_DIR')) { define('WC_TPP_PLUGIN_DIR', plugin_dir_path(__FILE__));