2025-12-31 00:38:29 +01:00
|
|
|
<?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 {
|
2025-12-31 16:41:53 +01:00
|
|
|
/**
|
|
|
|
|
* Stock manager instance
|
|
|
|
|
*
|
|
|
|
|
* @var Stock_Manager
|
|
|
|
|
*/
|
|
|
|
|
private $stock_manager;
|
|
|
|
|
|
2025-12-31 00:38:29 +01:00
|
|
|
/**
|
|
|
|
|
* Constructor
|
|
|
|
|
*/
|
|
|
|
|
public function __construct() {
|
2025-12-31 16:41:53 +01:00
|
|
|
$this->stock_manager = new Stock_Manager();
|
|
|
|
|
|
2025-12-31 00:38:29 +01:00
|
|
|
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);
|
2025-12-31 16:41:53 +01:00
|
|
|
add_action('woocommerce_checkout_create_order_line_item', [$this->stock_manager, 'store_selected_products_in_order'], 10, 3);
|
2025-12-31 00:38:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 16:41:53 +01:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 00:38:29 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 17:11:29 +01:00
|
|
|
// Use static flag to prevent multiple executions
|
|
|
|
|
static $already_calculated = false;
|
|
|
|
|
if ($already_calculated) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 00:38:29 +01:00
|
|
|
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
|
|
|
|
|
if (isset($cart_item['data']) && $cart_item['data']->get_type() === 'composable') {
|
2025-12-31 17:11:29 +01:00
|
|
|
if (isset($cart_item['composable_products']) && !isset($cart_item['composable_price_calculated'])) {
|
2025-12-31 00:38:29 +01:00
|
|
|
$product = $cart_item['data'];
|
|
|
|
|
$price = $product->calculate_composed_price($cart_item['composable_products']);
|
|
|
|
|
$cart_item['data']->set_price($price);
|
2025-12-31 17:11:29 +01:00
|
|
|
|
|
|
|
|
// Mark as calculated to prevent re-calculation by other plugins
|
|
|
|
|
$cart->cart_contents[$cart_item_key]['composable_price_calculated'] = true;
|
2025-12-31 00:38:29 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-31 17:11:29 +01:00
|
|
|
|
|
|
|
|
$already_calculated = true;
|
2025-12-31 00:38:29 +01:00
|
|
|
}
|
|
|
|
|
}
|