commerce_inventory-8.x-1.0-alpha6/modules/commerce_inventory_order/src/InventoryOrderManager.php
modules/commerce_inventory_order/src/InventoryOrderManager.php
<?php
namespace Drupal\commerce_inventory_order;
use Drupal\commerce_inventory\InventoryAllocationManager;
use Drupal\commerce_inventory\InventoryHelper;
use Drupal\commerce_order\Entity\OrderItemInterface;
use Drupal\commerce_order\Entity\OrderItemType;
use Drupal\commerce_order\Entity\OrderItemTypeInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* Provides an inventory manager for Order Item entities.
*/
class InventoryOrderManager {
/**
* The Commerce Inventory cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cacheFactory;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The chain inventory placement resolver.
*
* @var \Drupal\commerce_inventory\InventoryAllocationManager
*/
protected $inventoryAllocation;
/**
* The Inventory Adjustment entity storage.
*
* @var \Drupal\commerce_inventory\Entity\Storage\InventoryAdjustmentStorageInterface
*/
protected $inventoryAdjustmentStorage;
/**
* The Inventory Item entity storage.
*
* @var \Drupal\commerce_inventory\Entity\Storage\InventoryItemStorageInterface
*/
protected $inventoryItemStorage;
/**
* The Order Item entity storage.
*
* @var \Drupal\commerce_order\OrderItemStorageInterface
*/
protected $orderItemStorage;
/**
* The OrderType entity storage.
*
* @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface
*/
protected $orderTypeStorage;
/**
* Constructs a new OrderInventoryManager.
*
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_factory
* The Commerce Inventory cache backend.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity manager.
* @param \Drupal\commerce_inventory\InventoryAllocationManager $allocation_manager
* The inventory allocation manager.
*/
public function __construct(CacheBackendInterface $cache_factory, EntityTypeManagerInterface $entity_type_manager, InventoryAllocationManager $allocation_manager) {
$this->cacheFactory = $cache_factory;
$this->entityTypeManager = $entity_type_manager;
$this->inventoryAdjustmentStorage = $entity_type_manager->getStorage('commerce_inventory_adjustment');
$this->inventoryAllocation = $allocation_manager;
$this->inventoryItemStorage = $entity_type_manager->getStorage('commerce_inventory_item');
$this->orderItemStorage = $entity_type_manager->getStorage('commerce_order_item');
$this->orderTypeStorage = $entity_type_manager->getStorage('commerce_order_type');
}
/**
* Returns an the Order's selected adjustment workflow ID.
*
* @param \Drupal\commerce_order\Entity\OrderItemTypeInterface|string $order_item_type
* The Order Type ID.
*
* @return string
* The adjustment workflow ID.
*/
public static function getBundleInventoryWorkflowId($order_item_type) {
// Load order type if bundle id passed in.
if (is_string($order_item_type)) {
$order_item_type = OrderItemType::load($order_item_type);
}
// Return Order Type's setting.
if ($order_item_type instanceof OrderItemTypeInterface) {
return $order_item_type->getThirdPartySetting('commerce_inventory_order', 'inventory_workflow', 'default');
}
// Default to the 'default' id.
return 'default';
}
/**
* Returns an the Order's selected adjustment workflow ID.
*
* @param \Drupal\commerce_order\Entity\OrderItemTypeInterface|string $order_item_type
* The Order Type ID.
*
* @return array
* The inventory workflow transitions.
*/
public static function getBundleInventoryWorkflowTransitions($order_item_type) {
// Load order type if bundle id passed in.
if (is_string($order_item_type)) {
$order_item_type = OrderItemType::load($order_item_type);
}
// Return Order Type's setting.
if ($order_item_type instanceof OrderItemTypeInterface) {
return $order_item_type->getThirdPartySetting('commerce_inventory_order', 'inventory_workflow_transitions', []);
}
// Default to an empty array.
return [];
}
/**
* Returns all Order-to-Order-Item workflow transition relationships.
*
* @return array
* An array of relationships, keyed by bundle ID.
*/
public function getAllBundleInventoryWorkflowTransitions() {
$cid = 'inventory_workflow_transitions:order_item_type';
// Return cached bundle transition information.
if ($cache = $this->cacheFactory->get($cid)) {
$data = $cache->data;
}
// Compile all workflow transitions for each Order Item bundle.
else {
/** @var \Drupal\state_machine\WorkflowManagerInterface $workflow_manager */
$workflow_manager = \Drupal::service('plugin.manager.workflow');
$item_storage = $this->entityTypeManager->getStorage('commerce_order_item_type');
/** @var \Drupal\commerce_order\Entity\OrderItemTypeInterface[] $item_bundles */
$item_bundles = $item_storage->loadMultiple();
// Setup cache data and tags.
$data = [];
$cache_tags = $item_storage->getEntityType()->getListCacheTags();
// Get the Order Item bundles transitions.
$item_transitions = [];
foreach ($item_bundles as $item_bundle_id => $item_bundle) {
$item_transitions[$item_bundle_id] = self::getBundleInventoryWorkflowTransitions($item_bundle);
}
// Populate the workflow state and transitions.
foreach ($item_transitions as $item_bundle_id => $item_bundle_transitions) {
$item_bundle_worklow_id = InventoryOrderManager::getBundleInventoryWorkflowId($item_bundle_id);
/** @var \Drupal\state_machine\Plugin\Workflow\WorkflowInterface $item_bundle_worklow */
$item_bundle_worklow = $workflow_manager->createInstance($item_bundle_worklow_id);
// Key data by Order workflow transition ID, then by Order Item bundle.
foreach ($item_bundle_transitions as $order_transition_id => $item_transition_id) {
if ($item_transition = $item_bundle_worklow->getTransition($item_transition_id)) {
$data[$order_transition_id][$item_bundle_id] = [
'state' => $item_transition->getToState()->getId(),
'transition' => $item_transition_id,
];
}
}
}
// Set cache.
$this->cacheFactory->set($cid, $data, Cache::PERMANENT, $cache_tags);
}
return $data;
}
/**
* If this Order Item's inventory adjustments are allotted manually.
*
* @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
* The Order Item entity.
*
* @return bool
* True if the adjustments are allotted manually. False otherwise.
*/
public static function isAdjustedManually(OrderItemInterface $order_item) {
if ($order_item->get('inventory_adjustment_manual')->isEmpty()) {
return FALSE;
}
return $order_item->get('inventory_adjustment_manual')->get(0)->getValue()['value'] == 1;
}
/**
* Gets an Order Item's adjustment state ID.
*
* @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
* The Order Item entity.
*
* @return string
* The state ID.
*/
public static function getAdjustmentStateId(OrderItemInterface $order_item) {
if ($order_item->get('inventory_adjustment_state')->isEmpty()) {
return 'untracked';
}
return $order_item->get('inventory_adjustment_state')->get(0)->getValue()['value'];
}
/**
* Checks whether an Order Item is at a certain adjustment state.
*
* @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
* The Order Item entity.
* @param string $adjustment_state
* The adjustment state.
*
* @return bool
* Whether the Order is at a certain adjustment state.
*/
public static function checkAdjustmentState(OrderItemInterface $order_item, $adjustment_state) {
if ($order_item->get('inventory_adjustment_state')->isEmpty()) {
return FALSE;
}
return ($order_item->get('inventory_adjustment_state')->get(0)->getValue()['value'] == $adjustment_state);
}
/**
* Return created Inventory Adjustment quantities by Order Item.
*
* @param int $order_item_id
* The Order Item ID.
* @param bool $consolidate_by_item
* Whether the adjustments should be consolidated by Inventory Item ID.
*
* @return array
* The on-hand adjustments.
*/
public function getAdjustments($order_item_id, $consolidate_by_item = TRUE) {
// Pull previous on-hand inventory adjustments.
$adjustment_query = $this->inventoryAdjustmentStorage->getQuantitySelectQuery();
$adjustment_query->condition('order_item_id', $order_item_id);
// Consolidate quantity field.
if ($consolidate_by_item) {
// Remove quantity field since it's going to be generated.
unset($adjustment_query->getFields()['quantity']);
$adjustment_query->addExpression('sum(quantity)', 'quantity');
$adjustment_query->groupBy('inventory_item_id');
}
return $adjustment_query->execute()->fetchAll(\PDO::FETCH_ASSOC);
}
/**
* Apply the Order Item inventory workflow transition.
*
* @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
* The Order Item entity.
* @param string $transition_id
* The transition ID.
*
* @return bool
* Whether the transition was applied.
*/
public static function transitionAdjustmentState(OrderItemInterface $order_item, $transition_id) {
$state_field = $order_item->get('inventory_adjustment_state');
// Apply default value if not set.
if ($state_field->isEmpty()) {
$state_field->applyDefaultValue();
}
/** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItemInterface $state */
$state = $state_field->first();
$transitions = $state->getTransitions();
// Apply transition if it can be applied.
if (array_key_exists($transition_id, $transitions)) {
$state->applyTransition($transitions[$transition_id]);
return TRUE;
}
return FALSE;
}
/**
* Adjusts full on-hand Inventory count based on deleted Order Items.
*
* @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
* The Order Item entity.
* @param bool $save_adjustments
* Whether to save the adjustments before returning them.
*
* @return \Drupal\commerce_inventory\Entity\InventoryAdjustmentInterface[]
* The created Inventory Adjustments.
*/
public function deleteInventory(OrderItemInterface $order_item, $save_adjustments = TRUE) {
// Load previous adjustments.
$adjustments = $this->getAdjustments($order_item->id());
// Exit early if adjustments haven't been made.
if (empty($adjustments)) {
return [];
}
// Load Inventory Items.
$inventory_item_ids = array_column($adjustments, 'inventory_item_id');
/** @var \Drupal\commerce_inventory\Entity\InventoryItemInterface[] $inventory_items */
$inventory_items = $this->inventoryItemStorage->loadMultiple($inventory_item_ids);
// Create reusable values.
// @todo might want to make $adjustment_type_id dynamic based on adjustment quantity
$adjustment_type_id = 'return';
$adjustment_values = [
'order_id' => $order_item->getOrderId(),
'user_id' => $order_item->getOrder()->getCustomerId(),
];
// Convert each item to a new adjustment.
$inventory_adjustments = [];
foreach ($adjustments as $adjustment) {
if (array_key_exists($adjustment['inventory_item_id'], $inventory_items) && $adjustment['quantity'] <> 0) {
$inventory_adjustments[] = $this->inventoryAdjustmentStorage->createAdjustment($adjustment_type_id, $inventory_items[$adjustment['inventory_item_id']], $adjustment['quantity'], $adjustment_values, NULL, $save_adjustments);
}
}
return $inventory_adjustments;
}
/**
* Cancels held inventory placeholders.
*
* @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
* The Order Item entity.
*/
public function cancelInventoryHolds(OrderItemInterface $order_item) {
// Invalidate the previously used item cache tags.
if ($order_item->get('inventory_adjustment_holds')->isEmpty() == FALSE) {
foreach (array_column($order_item->get('inventory_adjustment_holds')->getValue(), 'target_id') as $inventory_item_id) {
$cid = InventoryHelper::generateQuantityCacheId($inventory_item_id, 'available');
$this->cacheFactory->invalidate($cid);
}
}
// Clear adjustment placeholders and settings.
$order_item->set('inventory_adjustment_holds', []);
$order_item->set('inventory_adjustment_manual', FALSE);
}
/**
* Adjusts full on-hand Inventory count based on updated Order Items.
*
* @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
* The Order Item entity.
* @param bool $save_adjustments
* Whether to save the adjustments before returning them.
*
* @return \Drupal\commerce_inventory\Entity\InventoryAdjustmentInterface[]
* The created Inventory Adjustments.
*/
public function convertInventoryHolds(OrderItemInterface $order_item, $save_adjustments = TRUE) {
// Exit early if order isn't set.
if (is_null($order_item->getOrderId())) {
return [];
}
// Initialize return.
$inventory_adjustments = [];
// Get previous inventory adjustments.
$adjustments_previous = $this->getAdjustments($order_item->id());
// Get relative quantity from previous full adjustments.
$quantity = floatval($order_item->getQuantity());
$quantity_previous = array_sum(array_column($adjustments_previous, 'quantity'));
$quantity_relative = $quantity + $quantity_previous;
// If quantity has changed.
if ($quantity_relative <> 0) {
// Get current adjustment holds to convert to on-hand adjustments.
$holds = $order_item->get('inventory_adjustment_holds')->getValue();
$inventory_item_ids = array_column($holds, 'target_id');
// Load Inventory Items.
/** @var \Drupal\commerce_inventory\Entity\InventoryItemInterface[] $inventory_items */
$inventory_items = $this->inventoryItemStorage->loadMultiple($inventory_item_ids);
// Create reusable values.
$adjustment_type_id = ($quantity_relative > 0) ? 'sell' : 'return';
$adjustment_values = [
'order_id' => $order_item->getOrderId(),
'order_item_id' => $order_item->id(),
'user_id' => $order_item->getOrder()->getCustomerId(),
];
// Convert each item to a new adjustment.
foreach ($holds as $adjustment) {
$item_id = $adjustment['target_id'];
$item_quantity = $adjustment['quantity'];
if (array_key_exists($item_id, $inventory_items) && $item_quantity <> 0) {
$inventory_adjustments[] = $this->inventoryAdjustmentStorage->createAdjustment($adjustment_type_id, $inventory_items[$item_id], $item_quantity, $adjustment_values, NULL, $save_adjustments);
}
}
// Invalidate items.
$cids = [];
foreach ($inventory_item_ids as $inventory_item_id) {
$cids[] = InventoryHelper::generateQuantityCacheId($inventory_item_id, 'available');
$cids[] = InventoryHelper::generateQuantityCacheId($inventory_item_id, 'on_hand');
}
$this->cacheFactory->invalidateMultiple($cids);
}
// Clear adjustment-related settings.
$order_item->set('inventory_adjustment_holds', []);
$order_item->set('inventory_adjustment_manual', FALSE);
// Default to an empty array.
return $inventory_adjustments;
}
/**
* Puts available inventory on hold for an Order Item entity.
*
* @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
* The Order Item entity.
*/
public function makeInventoryHolds(OrderItemInterface $order_item) {
// Get previous inventory adjustments.
$adjustments_previous = $this->getAdjustments($order_item->id());
// Get relative quantity from previous full adjustments.
$quantity = floatval($order_item->getQuantity());
$quantity_previous = array_sum(array_column($adjustments_previous, 'quantity'));
$quantity_relative = $quantity + $quantity_previous;
// If quantity has changed.
if ($quantity_relative <> 0) {
// Get current Order settings for allocation.
$context = [
'commerce_order_item' => $order_item,
'commerce_store' => $order_item->getOrder()->getStore(),
'inventory_adjustments' => $adjustments_previous,
];
$purchasable_entity = $order_item->getPurchasedEntity();
// Allocate inventory quantity that hasn't already been allocated to
// on-hand inventory. Passing in the relative quantity to on-hand
// adjustments allows for circumstances where on-hand Order Items need to
// be transitioned back to 'available' for validation before modifying
// on-hand inventory.
$allocation = $this->inventoryAllocation->allocate($purchasable_entity, $quantity_relative, $context);
// Convert values into inventory-quantity field values.
$quantity_data = array_map(function ($value) {
return [
'target_id' => $value['inventory_item_id'],
'quantity' => floatval($value['quantity']),
];
}, $allocation->toArray());
// Add item data to order item. (Don't save since it's ran on pre-save).
$order_item->set('inventory_adjustment_holds', $quantity_data);
// Use unmodified Order Item to load previous adjustments.
/** @var \Drupal\commerce_order\Entity\OrderItemInterface|null $unmodified_order_item */
$unmodified_order_item = (!is_null($order_item->id())) ? $this->orderItemStorage->load($order_item->id()) : NULL;
$unmodified_adjustment_item_ids = [];
if (!is_null($unmodified_order_item)) {
$unmodified_adjustment_item_ids = array_column($unmodified_order_item->get('inventory_adjustment_holds')->getValue(), 'target_id');
}
// Invalidate the previous and newly used item cache tags.
$ids_to_invalidate = array_merge($unmodified_adjustment_item_ids, array_column($quantity_data, 'target_id'));
foreach (array_unique($ids_to_invalidate) as $inventory_item_id) {
$cid = InventoryHelper::generateQuantityCacheId($inventory_item_id, 'available');
$this->cacheFactory->invalidate($cid);
}
}
}
}
