Files
wc-composable-product/includes/StockManager.php

284 lines
7.8 KiB
PHP
Raw Normal View History

<?php
/**
* Stock Manager
*
* @package Magdev\WcComposableProduct
*/
namespace Magdev\WcComposableProduct;
defined( 'ABSPATH' ) || exit;
/**
* Stock Manager Class
*
* Handles stock management for composable products
*/
class StockManager {
/**
* Constructor
*/
public function __construct() {
// Hook into order completion to reduce stock
add_action( 'woocommerce_order_status_completed', array( $this, 'reduce_stock_on_order_complete' ), 10, 1 );
add_action( 'woocommerce_order_status_processing', array( $this, 'reduce_stock_on_order_complete' ), 10, 1 );
// Hook into order cancellation/refund to restore stock
add_action( 'woocommerce_order_status_cancelled', array( $this, 'restore_stock_on_order_cancel' ), 10, 1 );
add_action( 'woocommerce_order_status_refunded', array( $this, 'restore_stock_on_order_cancel' ), 10, 1 );
// Prevent double stock reduction
add_filter( 'woocommerce_can_reduce_order_stock', array( $this, 'prevent_composable_stock_reduction' ), 10, 2 );
}
/**
* Validate stock availability for selected products
*
* @param array $selected_product_ids Array of product IDs
* @param int $quantity Quantity of composable product being added
* @return bool|string True if in stock, error message otherwise
*/
public function validate_stock_availability( $selected_product_ids, $quantity = 1 ) {
foreach ( $selected_product_ids as $product_id ) {
$product = wc_get_product( $product_id );
if ( ! $product ) {
continue;
}
// Skip stock check if stock management is disabled for this product
if ( ! $product->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 );
}
}
}