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 ( null !== $stock_quantity && $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 array( 'in_stock' => false, 'stock_quantity' => 0, 'backorders_allowed' => false, 'stock_status' => 'outofstock', ); } $stock_quantity = $product->get_stock_quantity(); $managing_stock = $product->managing_stock(); return array( '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 || null === $stock_quantity || $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 ( null !== $stock_quantity ) { $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 ( null !== $stock_quantity ) { $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 ); } } }