You've already forked wc-composable-product
Fixes three critical bugs reported in CLAUDE.md:
1. Admin rendering bug - Fixed CSS to prevent both General and Composable
Options tabs from showing simultaneously on initial page load
- Enhanced CSS specificity with !important flags
- Added body.product-type-composable selectors for proper visibility control
- Hides Composable Options tab by default, shows only when composable type selected
2. Frontend product selector not appearing - Fixed WooCommerce integration
- Added hide_default_add_to_cart() method to Cart_Handler
- Hooks woocommerce_is_purchasable filter to return false for composable products
- This hides WooCommerce's default add-to-cart button
- Allows our custom product selector to be the only interface
3. Localized price formatting - Implemented proper WooCommerce price formatting
- Added wc_price Twig function in Plugin.php
- Updated Product_Selector to pass formatted price HTML to template
- Added price_format data to JavaScript localization
- Implemented formatPrice() method in frontend.js
- Supports all WooCommerce price formats (currency position, decimals, separators)
- Template now uses {{ fixed_price_html|raw }} and {{ zero_price_html|raw }}
- JavaScript dynamically formats prices using locale-specific settings
Technical improvements:
- Cart_Handler.php: +14 lines (hide_default_add_to_cart method)
- Plugin.php: +7 lines (wc_price function, price format localization)
- Product_Selector.php: +2 lines (formatted price HTML context)
- templates/product-selector.twig: Modified to use formatted price HTML
- assets/css/admin.css: +24 lines (enhanced tab visibility control)
- assets/js/frontend.js: +28 lines (formatPrice method with WooCommerce format support)
All PHP files pass lint checks. Frontend now properly displays localized prices
with correct currency symbols, decimal separators, and thousand separators for
all WooCommerce-supported locales (CHF for Switzerland, € for Europe, etc.).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
225 lines
7.3 KiB
PHP
225 lines
7.3 KiB
PHP
<?php
|
|
/**
|
|
* Cart Handler
|
|
*
|
|
* @package WC_Composable_Product
|
|
*/
|
|
|
|
namespace WC_Composable_Product;
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
/**
|
|
* Cart Handler Class
|
|
*
|
|
* Handles adding composable products to cart and calculating prices
|
|
*/
|
|
class Cart_Handler {
|
|
/**
|
|
* Stock manager instance
|
|
*
|
|
* @var Stock_Manager
|
|
*/
|
|
private $stock_manager;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct() {
|
|
$this->stock_manager = new Stock_Manager();
|
|
|
|
add_filter('woocommerce_add_to_cart_validation', [$this, 'validate_add_to_cart'], 10, 3);
|
|
add_filter('woocommerce_add_cart_item_data', [$this, 'add_cart_item_data'], 10, 2);
|
|
add_filter('woocommerce_get_cart_item_from_session', [$this, 'get_cart_item_from_session'], 10, 2);
|
|
add_filter('woocommerce_get_item_data', [$this, 'display_cart_item_data'], 10, 2);
|
|
add_action('woocommerce_before_calculate_totals', [$this, 'calculate_cart_item_price']);
|
|
add_action('woocommerce_single_product_summary', [$this, 'render_product_selector'], 25);
|
|
add_action('woocommerce_checkout_create_order_line_item', [$this->stock_manager, 'store_selected_products_in_order'], 10, 3);
|
|
add_filter('woocommerce_is_purchasable', [$this, 'hide_default_add_to_cart'], 10, 2);
|
|
}
|
|
|
|
/**
|
|
* Hide default WooCommerce add to cart button for composable products
|
|
*
|
|
* @param bool $is_purchasable Is purchasable status
|
|
* @param \WC_Product $product Product object
|
|
* @return bool
|
|
*/
|
|
public function hide_default_add_to_cart($is_purchasable, $product) {
|
|
if ($product && $product->get_type() === 'composable') {
|
|
return false;
|
|
}
|
|
return $is_purchasable;
|
|
}
|
|
|
|
/**
|
|
* Render product selector on product page
|
|
*/
|
|
public function render_product_selector() {
|
|
global $product;
|
|
|
|
if ($product && $product->get_type() === 'composable') {
|
|
Product_Selector::render($product);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate add to cart
|
|
*
|
|
* @param bool $passed Validation status
|
|
* @param int $product_id Product ID
|
|
* @param int $quantity Quantity
|
|
* @return bool
|
|
*/
|
|
public function validate_add_to_cart($passed, $product_id, $quantity) {
|
|
$product = wc_get_product($product_id);
|
|
|
|
if (!$product || $product->get_type() !== 'composable') {
|
|
return $passed;
|
|
}
|
|
|
|
// Check if selected products are provided
|
|
if (!isset($_POST['composable_products']) || empty($_POST['composable_products'])) {
|
|
wc_add_notice(__('Please select at least one product.', 'wc-composable-product'), 'error');
|
|
return false;
|
|
}
|
|
|
|
$selected_products = array_map('absint', $_POST['composable_products']);
|
|
$selection_limit = $product->get_selection_limit();
|
|
|
|
// Validate selection limit
|
|
if (count($selected_products) > $selection_limit) {
|
|
/* translators: %d: selection limit */
|
|
wc_add_notice(sprintf(__('You can select a maximum of %d products.', 'wc-composable-product'), $selection_limit), 'error');
|
|
return false;
|
|
}
|
|
|
|
if (count($selected_products) === 0) {
|
|
wc_add_notice(__('Please select at least one product.', 'wc-composable-product'), 'error');
|
|
return false;
|
|
}
|
|
|
|
// Validate that selected products are valid
|
|
$available_products = $product->get_available_products();
|
|
$available_ids = array_map(function($p) {
|
|
return $p->get_id();
|
|
}, $available_products);
|
|
|
|
foreach ($selected_products as $selected_id) {
|
|
if (!in_array($selected_id, $available_ids)) {
|
|
wc_add_notice(__('One or more selected products are not available.', 'wc-composable-product'), 'error');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Validate stock availability
|
|
$stock_validation = $this->stock_manager->validate_stock_availability($selected_products, $quantity);
|
|
if ($stock_validation !== true) {
|
|
wc_add_notice($stock_validation, 'error');
|
|
return false;
|
|
}
|
|
|
|
return $passed;
|
|
}
|
|
|
|
/**
|
|
* Add cart item data
|
|
*
|
|
* @param array $cart_item_data Cart item data
|
|
* @param int $product_id Product ID
|
|
* @return array
|
|
*/
|
|
public function add_cart_item_data($cart_item_data, $product_id) {
|
|
$product = wc_get_product($product_id);
|
|
|
|
if (!$product || $product->get_type() !== 'composable') {
|
|
return $cart_item_data;
|
|
}
|
|
|
|
if (isset($_POST['composable_products']) && !empty($_POST['composable_products'])) {
|
|
$selected_products = array_map('absint', $_POST['composable_products']);
|
|
$cart_item_data['composable_products'] = $selected_products;
|
|
|
|
// Make cart item unique
|
|
$cart_item_data['unique_key'] = md5(json_encode($selected_products) . time());
|
|
}
|
|
|
|
return $cart_item_data;
|
|
}
|
|
|
|
/**
|
|
* Get cart item from session
|
|
*
|
|
* @param array $cart_item Cart item
|
|
* @param array $values Values from session
|
|
* @return array
|
|
*/
|
|
public function get_cart_item_from_session($cart_item, $values) {
|
|
if (isset($values['composable_products'])) {
|
|
$cart_item['composable_products'] = $values['composable_products'];
|
|
}
|
|
|
|
return $cart_item;
|
|
}
|
|
|
|
/**
|
|
* Display cart item data
|
|
*
|
|
* @param array $item_data Item data
|
|
* @param array $cart_item Cart item
|
|
* @return array
|
|
*/
|
|
public function display_cart_item_data($item_data, $cart_item) {
|
|
if (isset($cart_item['composable_products']) && !empty($cart_item['composable_products'])) {
|
|
$product_names = [];
|
|
foreach ($cart_item['composable_products'] as $product_id) {
|
|
$product = wc_get_product($product_id);
|
|
if ($product) {
|
|
$product_names[] = $product->get_name();
|
|
}
|
|
}
|
|
|
|
if (!empty($product_names)) {
|
|
$item_data[] = [
|
|
'key' => __('Selected Products', 'wc-composable-product'),
|
|
'value' => implode(', ', $product_names),
|
|
];
|
|
}
|
|
}
|
|
|
|
return $item_data;
|
|
}
|
|
|
|
/**
|
|
* Calculate cart item price
|
|
*
|
|
* @param \WC_Cart $cart Cart object
|
|
*/
|
|
public function calculate_cart_item_price($cart) {
|
|
if (is_admin() && !defined('DOING_AJAX')) {
|
|
return;
|
|
}
|
|
|
|
// Use static flag to prevent multiple executions
|
|
static $already_calculated = false;
|
|
if ($already_calculated) {
|
|
return;
|
|
}
|
|
|
|
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
|
|
if (isset($cart_item['data']) && $cart_item['data']->get_type() === 'composable') {
|
|
if (isset($cart_item['composable_products']) && !isset($cart_item['composable_price_calculated'])) {
|
|
$product = $cart_item['data'];
|
|
$price = $product->calculate_composed_price($cart_item['composable_products']);
|
|
$cart_item['data']->set_price($price);
|
|
|
|
// Mark as calculated to prevent re-calculation by other plugins
|
|
$cart->cart_contents[$cart_item_key]['composable_price_calculated'] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
$already_calculated = true;
|
|
}
|
|
}
|