arch-8.x-1.x-dev/modules/stock/src/StockKeeper.php
modules/stock/src/StockKeeper.php
<?php
namespace Drupal\arch_stock;
use Drupal\arch_order\Entity\OrderInterface;
use Drupal\arch_product\Entity\ProductAvailability;
use Drupal\arch_product\Entity\ProductInterface;
use Drupal\arch_stock\Entity\WarehouseInterface;
use Drupal\arch_stock\Manager\WarehouseManagerInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Stock keeper service.
*
* @package Drupal\arch_stock
*/
class StockKeeper implements StockKeeperInterface {
/**
* Product type storage.
*
* @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface
*/
protected $productTypeStorage;
/**
* Warehouse manager.
*
* @var \Drupal\arch_stock\Manager\WarehouseManagerInterface
*/
protected $warehouseManager;
/**
* Module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Cache tags invalidator service.
*
* @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
*/
protected $cacheTagsInvalidator;
/**
* Lock backend.
*
* @var \Drupal\Core\Lock\LockBackendInterface
*/
protected $lock;
/**
* StockKeeper constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* Entity type manager.
* @param \Drupal\arch_stock\Manager\WarehouseManagerInterface $warehouse_manager
* Warehouse manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* Module handler.
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator
* Cache tag invalidator service.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* Lock backend.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function __construct(
EntityTypeManagerInterface $entity_type_manager,
WarehouseManagerInterface $warehouse_manager,
ModuleHandlerInterface $module_handler,
CacheTagsInvalidatorInterface $cache_tags_invalidator,
LockBackendInterface $lock,
) {
$this->productTypeStorage = $entity_type_manager->getStorage('product_type');
$this->warehouseManager = $warehouse_manager;
$this->moduleHandler = $module_handler;
$this->cacheTagsInvalidator = $cache_tags_invalidator;
$this->lock = $lock;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('warehouse.manager'),
$container->get('module_handler'),
$container->get('cache_tags.invalidator'),
$container->get('lock')
);
}
/**
* {@inheritdoc}
*/
public function isProductManagingStock(ProductInterface $product) {
/** @var \Drupal\arch_product\Entity\ProductTypeInterface $type */
$type = $this->productTypeStorage->load($product->bundle());
return (bool) $type->getThirdPartySetting('arch_stock', 'stock_enable');
}
/**
* {@inheritdoc}
*/
public function reduceStock(
ProductInterface $product,
$sold_amount,
OrderInterface $order,
AccountInterface $account,
) {
$lock_name = 'arch_stock_reduce:product:' . $product->id();
while ($this->lock->wait($lock_name, 0.2)) {
$this->lock->acquire($lock_name);
$this->doReduce($product, $sold_amount, $order, $account);
$this->lock->release($lock_name);
break;
}
}
/**
* {@inheritdoc}
*/
public function isNegativeStockAllowed(
ProductInterface $product,
AccountInterface $account,
) {
if ($product->getAvailability() === ProductAvailability::STATUS_NOT_AVAILABLE) {
return FALSE;
}
$selected_warehouses = $this->selectWarehouses($account);
return $this->allowNegativeStockForWarehouses($selected_warehouses, $account);
}
/**
* {@inheritdoc}
*/
public function getTotalProductStock(
ProductInterface $product,
AccountInterface $account,
) {
$selected_warehouses = $this->selectWarehouses($account);
$stock_values = $this->getCurrentStock($product);
$total = 0;
foreach ($stock_values as &$stock) {
if (!in_array($stock['warehouse'], $selected_warehouses)) {
continue;
}
$total += $stock['quantity'];
}
$this->moduleHandler->alter('product_stock', $total, $product, $account);
return $total;
}
/**
* {@inheritdoc}
*/
public function hasProductEnoughStock(
ProductInterface $product,
AccountInterface $account,
$amount = 1,
) {
if ($product->getAvailability() === ProductAvailability::STATUS_NOT_AVAILABLE) {
return FALSE;
}
$selected_warehouses = $this->selectWarehouses($account);
if ($this->allowNegativeStockForWarehouses($selected_warehouses, $account)) {
return TRUE;
}
return $this->getTotalProductStock($product, $account) >= $amount;
}
/**
* Do reduce.
*
* @param \Drupal\arch_product\Entity\ProductInterface $product
* Product entity.
* @param float $sold_amount
* Sold amount of product.
* @param \Drupal\arch_order\Entity\OrderInterface $order
* Order entity.
* @param \Drupal\Core\Session\AccountInterface $account
* Customer account.
*
* @return bool
* Result.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
protected function doReduce(
ProductInterface $product,
$sold_amount,
OrderInterface $order,
AccountInterface $account,
) {
$selected_warehouses = $this->selectWarehouses($account);
$amount = $sold_amount;
$stock_values = $this->getCurrentStock($product);
$original_stock_values = $stock_values;
foreach ($stock_values as &$stock) {
if (!in_array($stock['warehouse'], $selected_warehouses)) {
continue;
}
$available = $stock['quantity'];
if ($available <= $amount) {
$stock['quantity'] = 0;
$amount -= $available;
}
else {
$stock['quantity'] -= $amount;
$amount = 0;
}
}
if ($amount > 0) {
if ($this->allowNegativeStockForWarehouses($selected_warehouses, $account)) {
/** @var \Drupal\arch_stock\Entity\WarehouseInterface[] $warehouses */
$warehouses = $this->warehouseManager->getAvailableWarehouses($account);
foreach ($stock_values as &$stock) {
if (!in_array($stock['warehouse'], $selected_warehouses)) {
continue;
}
if (
!empty($stock['warehouse'])
&& !empty($warehouses[$stock['warehouse']])
&& $warehouses[$stock['warehouse']]->allowNegative()
) {
$stock['quantity'] -= $amount;
$amount = 0;
if (
$stock['quantity'] <= 0
&& ($availability = $warehouses[$stock['warehouse']]->getOverBookedAvailability())
) {
$product->setAvailability($availability);
}
}
}
}
// @todo Handle over selling.
}
/** @var \Drupal\arch_product\Entity\ProductInterface $product */
$product->set('stock', $stock_values);
$product->setNewRevision(FALSE);
$product->save();
$this->moduleHandler->invokeAll('stock_reduced', [
$sold_amount,
$product,
$order,
$account,
$original_stock_values,
]);
// Invalidate cache.
$this->cacheTagsInvalidator->invalidateTags($product->getCacheTagsToInvalidate());
return TRUE;
}
/**
* Get current stock of product.
*
* @param \Drupal\arch_product\Entity\ProductInterface $product
* Product entity.
*
* @return array
* Stock values.
*/
protected function getCurrentStock(ProductInterface $product) {
if (!$product->hasField('stock')) {
return [];
}
return array_map(function ($item) {
$item['quantity'] = (float) $item['quantity'];
return $item;
}, $product->get('stock')->getValue());
}
/**
* {@inheritdoc}
*/
public function selectWarehouses(AccountInterface $account) {
$warehouses = $this->warehouseManager->getAvailableWarehouses($account);
$list = array_map(function ($warehouse) {
/** @var \Drupal\arch_stock\Entity\WarehouseInterface $warehouse */
return $warehouse->id();
}, $warehouses);
$context = [
'account' => $account,
'warehouses' => $warehouses,
];
$this->moduleHandler->alter('stock_keeper_selected_warehouses', $list, $context);
return $list;
}
/**
* Check if any of the warehouses with given IDs is allow negative stock.
*
* @param string[] $warehouse_ids
* Warehouse IDs.
* @param \Drupal\Core\Session\AccountInterface $account
* Account.
*
* @return bool
* Return TRUE if any of the warehouses with given ID is allow overbooking.
*/
protected function allowNegativeStockForWarehouses(array $warehouse_ids, AccountInterface $account) {
if (empty($warehouse_ids)) {
return FALSE;
}
$warehouses = $this->warehouseManager->getWarehouses();
foreach ($warehouses as $warehouse) {
if (!in_array($warehouse->id(), $warehouse_ids)) {
continue;
}
if ($this->allowNegativeStockForWarehouse($warehouse, $account)) {
return TRUE;
}
}
return FALSE;
}
/**
* Check if negative stock is allowed for account at warehouse.
*
* @param \Drupal\arch_stock\Entity\WarehouseInterface $warehouse
* Warehouse entity.
* @param \Drupal\Core\Session\AccountInterface $account
* Account.
*
* @return bool
* Returns TRUE if given user can access negative stock in warehouse.
*/
protected function allowNegativeStockForWarehouse(WarehouseInterface $warehouse, AccountInterface $account) {
$allow_negative = AccessResult::allowedIf($warehouse->allowNegative());
$this->moduleHandler->alter('allow_negative_stock_for_warehouse', $allow_negative, $warehouse, $account);
return $allow_negative->isAllowed();
}
}
