managing_stock()) { continue; } $stock_quantity = $product->get_stock_quantity(); // Check if product is in stock if (!$product->is_in_stock()) { return sprintf( /* translators: %s: product name */ __('"%s" is out of stock and cannot be selected.', 'wc-composable-product'), $product->get_name() ); } // Check if enough stock is available if ($stock_quantity !== null && $stock_quantity < $quantity) { return sprintf( /* translators: 1: product name, 2: stock quantity */ __('Only %2$d of "%1$s" are available in stock.', 'wc-composable-product'), $product->get_name(), $stock_quantity ); } // Check for backorders if ($product->backorders_allowed()) { continue; } } return true; } /** * Check if a product has sufficient stock * * @param int $product_id Product ID * @param int $required_quantity Required quantity * @return array Stock information [in_stock, stock_quantity, backorders_allowed] */ public function get_product_stock_info($product_id, $required_quantity = 1) { $product = wc_get_product($product_id); if (!$product) { return [ 'in_stock' => false, 'stock_quantity' => 0, 'backorders_allowed' => false, 'stock_status' => 'outofstock', ]; } $stock_quantity = $product->get_stock_quantity(); $managing_stock = $product->managing_stock(); return [ 'in_stock' => $product->is_in_stock(), 'stock_quantity' => $stock_quantity, 'backorders_allowed' => $product->backorders_allowed(), 'stock_status' => $product->get_stock_status(), 'managing_stock' => $managing_stock, 'has_enough_stock' => !$managing_stock || $stock_quantity === null || $stock_quantity >= $required_quantity, ]; } /** * Reduce stock for composable products when order is completed * * @param int $order_id Order ID */ public function reduce_stock_on_order_complete($order_id) { $order = wc_get_order($order_id); if (!$order) { return; } // Check if stock has already been reduced if ($order->get_meta('_composable_stock_reduced', true)) { return; } foreach ($order->get_items() as $item) { $product = $item->get_product(); if (!$product || $product->get_type() !== 'composable') { continue; } // Get selected products from order item meta $selected_products = $item->get_meta('_composable_products', true); if (empty($selected_products) || !is_array($selected_products)) { continue; } $quantity = $item->get_quantity(); // Reduce stock for each selected product foreach ($selected_products as $product_id) { $selected_product = wc_get_product($product_id); if (!$selected_product || !$selected_product->managing_stock()) { continue; } $stock_quantity = $selected_product->get_stock_quantity(); if ($stock_quantity !== null) { $new_stock = $stock_quantity - $quantity; $selected_product->set_stock_quantity($new_stock); $selected_product->save(); // Add order note $order->add_order_note( sprintf( /* translators: 1: product name, 2: quantity, 3: remaining stock */ __('Stock reduced for "%1$s": -%2$d (remaining: %3$d)', 'wc-composable-product'), $selected_product->get_name(), $quantity, $new_stock ) ); } } } // Mark stock as reduced $order->update_meta_data('_composable_stock_reduced', true); $order->save(); } /** * Restore stock when order is cancelled or refunded * * @param int $order_id Order ID */ public function restore_stock_on_order_cancel($order_id) { $order = wc_get_order($order_id); if (!$order) { return; } // Check if stock was reduced if (!$order->get_meta('_composable_stock_reduced', true)) { return; } foreach ($order->get_items() as $item) { $product = $item->get_product(); if (!$product || $product->get_type() !== 'composable') { continue; } // Get selected products from order item meta $selected_products = $item->get_meta('_composable_products', true); if (empty($selected_products) || !is_array($selected_products)) { continue; } $quantity = $item->get_quantity(); // Restore stock for each selected product foreach ($selected_products as $product_id) { $selected_product = wc_get_product($product_id); if (!$selected_product || !$selected_product->managing_stock()) { continue; } $stock_quantity = $selected_product->get_stock_quantity(); if ($stock_quantity !== null) { $new_stock = $stock_quantity + $quantity; $selected_product->set_stock_quantity($new_stock); $selected_product->save(); // Add order note $order->add_order_note( sprintf( /* translators: 1: product name, 2: quantity, 3: new stock */ __('Stock restored for "%1$s": +%2$d (total: %3$d)', 'wc-composable-product'), $selected_product->get_name(), $quantity, $new_stock ) ); } } } // Mark stock as restored $order->update_meta_data('_composable_stock_reduced', false); $order->save(); } /** * Prevent WooCommerce from reducing stock for composable products * We handle stock reduction manually for selected products * * @param bool $reduce_stock Whether to reduce stock * @param \WC_Order $order Order object * @return bool */ public function prevent_composable_stock_reduction($reduce_stock, $order) { foreach ($order->get_items() as $item) { $product = $item->get_product(); if ($product && $product->get_type() === 'composable') { // We'll handle stock reduction manually return false; } } return $reduce_stock; } /** * Store selected products in order item meta * * @param \WC_Order_Item_Product $item Order item * @param string $cart_item_key Cart item key * @param array $values Cart item values */ public function store_selected_products_in_order($item, $cart_item_key, $values) { if (isset($values['composable_products']) && !empty($values['composable_products'])) { $item->add_meta_data('_composable_products', $values['composable_products'], true); } } }