<?php
declare(strict_types=1);
namespace Zeobv\GetNotified\FlowBuilder\Subscriber;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\ResultStatement;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
use Shopware\Core\Checkout\Order\OrderEvents;
use Shopware\Core\Checkout\Order\OrderStates;
use Shopware\Core\Content\Product\Events\ProductIndexerEvent;
use Shopware\Core\Content\Product\ProductEvents;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\DataAbstractionLayer\EntityWriteResult;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\StateMachine\Event\StateMachineTransitionEvent;
use Zeobv\GetNotified\FlowBuilder\Event\ProductBackInStockEvent;
use Zeobv\GetNotified\Service\ConfigService;
class StockChangesSubscriber implements EventSubscriberInterface
{
protected ConfigService $configService;
protected Connection $connection;
protected EventDispatcherInterface $dispatcher;
private LoggerInterface $logger;
public function __construct(
ConfigService $configService,
Connection $connection,
EventDispatcherInterface $dispatcher,
LoggerInterface $logger
) {
$this->configService = $configService;
$this->connection = $connection;
$this->dispatcher = $dispatcher;
$this->logger = $logger;
}
public static function getSubscribedEvents(): array
{
return [
ProductEvents::PRODUCT_INDEXER_EVENT => ['productIndex', -1000],
StateMachineTransitionEvent::class => ['stateChanged', -1000],
OrderEvents::ORDER_LINE_ITEM_WRITTEN_EVENT => ['lineItemWritten', -1000],
OrderEvents::ORDER_LINE_ITEM_DELETED_EVENT => ['lineItemWritten', -1000],
];
}
public function productIndex(ProductIndexerEvent $event): void
{
$this->checkStockAvailability($event->getIds());
}
/**
* If the product of an order item changed, it's possible for a product to come back in stock
*/
public function lineItemWritten(EntityWrittenEvent $event): void
{
$ids = [];
foreach ($event->getWriteResults() as $result) {
if ($result->hasPayload('referencedId') && $result->getProperty('type') === LineItem::PRODUCT_LINE_ITEM_TYPE) {
$ids[] = $result->getProperty('referencedId');
}
if ($result->getOperation() === EntityWriteResult::OPERATION_INSERT) {
continue;
}
$changeSet = $result->getChangeSet();
if (!$changeSet) {
continue;
}
if ($result->getOperation() === EntityWriteResult::OPERATION_DELETE) {
$ids[] = $changeSet->getBefore('referenced_id');
$ids[] = $changeSet->getAfter('referenced_id');
continue;
}
$type = $changeSet->getBefore('type');
if ($type !== LineItem::PRODUCT_LINE_ITEM_TYPE) {
continue;
}
if (!$changeSet->hasChanged('referenced_id') && !$changeSet->hasChanged('quantity')) {
continue;
}
// If the quantity is increased(meaning the stock decreased) we don't need to trigger a back in stock event
if ($changeSet->getBefore('quantity') ?? 0 <= $changeSet->getAfter('quantity') ?? 0) {
continue;
}
$ids[] = $changeSet->getAfter('referenced_id');
}
$ids = array_filter(array_unique($ids));
if (empty($ids)) {
return;
}
$this->checkStockAvailability($ids);
}
/**
* Triggered on order state changes
*/
public function stateChanged(StateMachineTransitionEvent $event): void
{
if ($event->getContext()->getVersionId() !== Defaults::LIVE_VERSION) {
return;
}
if ($event->getEntityName() !== 'order') {
return;
}
# only cancelled can lead to a stock increase
if ($event->getToPlace()->getTechnicalName() !== OrderStates::STATE_CANCELLED) {
return;
}
$ids = $this->getProductIdsOfOrder($event->getEntityId());
$this->checkStockAvailability($ids);
}
private function checkStockAvailability(array $ids): void
{
$result = $this->getProductStockInformation($ids);
$useAvailableStock = $this->configService->getUseAvailableStock();
foreach ($result as $productStockData) {
$stock = $useAvailableStock ? $productStockData['available_stock'] : $productStockData['stock'];
if ($stock < 1) {
continue;
}
$this->dispatcher->dispatch(new ProductBackInStockEvent(
Uuid::fromBytesToHex($productStockData['id'])
));
}
}
private function getProductIdsOfOrder(string $orderId): array
{
$query = $this->connection->createQueryBuilder();
$query->select(['referenced_id']);
$query->from('order_line_item');
$query->andWhere('type = :type');
$query->andWhere('order_id = :id');
$query->andWhere('version_id = :version');
$query->setParameter('id', Uuid::fromHexToBytes($orderId));
$query->setParameter('version', Uuid::fromHexToBytes(Defaults::LIVE_VERSION));
$query->setParameter('type', LineItem::PRODUCT_LINE_ITEM_TYPE);
try {
/** @var ResultStatement $statement */
$statement = $query->execute();
$result = $statement->fetchAll();
return array_column($result, 'referenced_id');
} catch (\Throwable $e) {
$this->logger->error($e->getMessage(), ['exception' => $e->getTrace()]);
return [];
}
}
private function getProductStockInformation(array $productIds): array
{
$query = $this->connection->createQueryBuilder();
$query->select(['product.id as id', 'stock', 'available_stock']);
$query->from('product');
$query->rightJoin('product', 'zeo_stock_subscriber_product', 'zsp', 'zsp.product_id = product.id');
$query->where('product.id IN (:ids)');
$query->groupBy('product.id');
$query->setMaxResults(count($productIds));
$query->setParameter('ids', Uuid::fromHexToBytesList($productIds), Connection::PARAM_STR_ARRAY);
try {
/** @var ResultStatement $statement */
$statement = $query->execute();
return $statement->fetchAll();
} catch (\Throwable $e) {
$this->logger->error($e->getMessage(), ['exception' => $e->getTrace()]);
return [];
}
}
}