<?php declare(strict_types=1);
namespace App\Stock\Service;
use App\Framework\Exception\ValidationException;
use App\Order\Event\OrderItemsUpdatedEvent;
use App\Product\Entity\Product;
use App\Product\Service\ProductService;
use App\Stock\Entity\StockItem;
use App\Stock\Entity\StockMovement;
use App\Stock\Event\StockChangedEvent;
use App\Stock\Message\ProductOutOfStockEvent;
use App\Stock\Repository\StockItemRepository;
use App\Stock\Repository\StockMovementRepository;
use Doctrine\ORM\ORMException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class StockService implements EventSubscriberInterface
{
private StockItemRepository $repository;
private StockMovementRepository $stockMovementRepository;
private EventDispatcherInterface $dispatcher;
private ValidatorInterface $validator;
private ProductService $productService;
private MessageBusInterface $eventBus;
public function __construct(
StockItemRepository $repository,
StockMovementRepository $stockMovementRepository,
ProductService $productService,
EventDispatcherInterface $dispatcher,
ValidatorInterface $validator,
MessageBusInterface $eventBus
) {
$this->repository = $repository;
$this->stockMovementRepository = $stockMovementRepository;
$this->dispatcher = $dispatcher;
$this->productService = $productService;
$this->validator = $validator;
$this->eventBus = $eventBus;
}
/**
* @return array The event names to listen to
*/
public static function getSubscribedEvents(): array
{
return [
OrderItemsUpdatedEvent::class => 'onOrderItemsUpdated',
StockChangedEvent::class => 'onStockChanged',
];
}
/**
* @throws ORMException
*/
public function addStock(StockItem $stockItem, int $amount, string $comment = ''): void
{
// Update stock
$prevStock = $stockItem->getCurrentStock();
$stockItem->setCurrentStock($prevStock + $amount);
// Flush
$this->repository->flush();
// Dispatch event
$this->dispatcher->dispatch(new StockChangedEvent($stockItem, $prevStock, $prevStock + $amount, $comment));
}
/**
* @throws ORMException
*/
public function removeStock(StockItem $stockItem, int $amount, string $comment): void
{
// Update the stock of the base product
$prevStock = $stockItem->getCurrentStock();
$stockItem->setCurrentStock($prevStock - $amount);
// Flush
$this->repository->flush();
// Dispatch event
$this->dispatcher->dispatch(new StockChangedEvent($stockItem, $prevStock, $prevStock - $amount, $comment));
}
/**
* @return StockItem[]
*/
public function getStockList(): array
{
return $this->repository->findAllSorted();
}
public function getStockByProduct(Product $product): ?StockItem
{
return $this->repository->findByProductId($product->getId());
}
/**
* @return StockMovement[]
*/
public function getStockMovementsByProductLastTwelveMonths(Product $product): array {
return $this->stockMovementRepository->findByProductIdLastTwelveMonths($product->getId());
}
public function createFromFields(array $fields): StockItem
{
$stockItem = new StockItem();
$fields = array_intersect_key(
$fields,
array_flip(['product', 'minimumStockLevel', 'stopSellingStockLevel', 'perishable'])
);
$accessor = PropertyAccess::createPropertyAccessor();
foreach ($fields as $key => $value) {
$accessor->setValue($stockItem, $key, $value);
}
$errors = $this->validator->validate($stockItem);
if (count($errors) > 0) {
throw new ValidationException($errors);
}
$this->repository->add($stockItem, true);
return $stockItem;
}
public function updateFields(StockItem $stockItem, array $fields): void
{
$fields = array_intersect_key(
$fields,
array_flip(['minimumStockLevel', 'stopSellingStockLevel', 'perishable'])
);
// Update each modified field
$accessor = PropertyAccess::createPropertyAccessor();
foreach ($fields as $key => $value) {
$accessor->setValue($stockItem, $key, $value);
}
$errors = $this->validator->validate($stockItem);
if (count($errors) > 0) {
throw new ValidationException($errors);
}
// flush
$this->repository->flush();
}
public function onOrderItemsUpdated(OrderItemsUpdatedEvent $event): void
{
$comment = 'Modificación Pedido ' . $event->getOrder()->getId();
$items = $event->getVariations();
foreach ($items as $key => $item) {
$product = $item['product'];
$variation = $item['variation'];
$stockItem = $this->getStockByProduct($product);
if ($stockItem !== null) {
if ($variation > 0) {
$this->removeStock($stockItem, $variation, $comment);
} else {
$this->addStock($stockItem, $variation, $comment);
}
}
}
}
/**
* @throws ORMException
*/
public function onStockChanged(StockChangedEvent $event): void
{
// Register the stock movement
$movement = new StockMovement();
$movement->setStockItem($event->getStockItem());
$movement->setBalance($event->getNewStock());
$movement->setComment($event->getComment());
$movement->setQuantity($event->getNewStock() - $event->getPreviousStock());
$this->stockMovementRepository->add($movement, true);
// Disable public sale of product if stock goes below a certain level
$stockItem = $event->getStockItem();
if ($event->getNewStock() < $stockItem->getStopSellingStockLevel()) {
$this->productService->stopSellingProduct($stockItem->getProduct());
$this->eventBus->dispatch(new ProductOutOfStockEvent($stockItem->getProduct()->getId()));
}
}
}