tetssssss

This commit is contained in:
lubiana 2025-06-10 19:05:55 +02:00
parent 6f07d70436
commit 8063e7bec9
Signed by: lubiana
SSH key fingerprint: SHA256:vW1EA0fRR3Fw+dD/sM0K+x3Il2gSry6YRYHqOeQwrfk
28 changed files with 771 additions and 273 deletions

View file

@ -58,8 +58,10 @@ final class DrinkTypeController extends AbstractController
$desiredStockHistory = $propertyChangeLogRepository->findBy([
'entityClass' => DrinkType::class,
'propertyName' => 'desiredStock',
'entityId' => $drinkType->getId()
], ['changeDate' => 'DESC']);
'entityId' => $drinkType->getId(),
], [
'changeDate' => 'DESC',
]);
return $this->render('drink_type/show.html.twig', [
'drink_type' => $drinkType,

View file

@ -4,9 +4,6 @@ declare(strict_types=1);
namespace App\Controller;
use App\Enum\StockState;
use App\Service\InventoryService;
use App\ValueObject\DrinkStock;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@ -14,19 +11,8 @@ use Symfony\Component\Routing\Attribute\Route;
#[Route(path: '/', name: 'app_index')]
final class Index extends AbstractController
{
public function __invoke(): Response
{
$drinkStocks = $this->inventoryService->getAllDrinkTypesWithStockLevels();
$low = array_filter(
$drinkStocks,
fn(DrinkStock $stock): bool => $stock->stock === StockState::LOW || $stock->stock === StockState::CRITICAL,
);
return $this->render('index.html.twig', [
'drinkStocks' => $drinkStocks,
'low' => $low,
]);
return new Response('<h1>Hello World!</h1>');
}
}

View file

@ -6,13 +6,14 @@ namespace App\Entity;
use App\Repository\PropertyChangeLogRepository;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use DateTimeImmutable;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Table;
#[ORM\Entity(repositoryClass: PropertyChangeLogRepository::class)]
#[ORM\Table(name: 'property_change_log')]
#[Entity(repositoryClass: PropertyChangeLogRepository::class)]
#[Table(name: 'property_change_log')]
final class PropertyChangeLog
{
#[Id]

View file

@ -1,21 +1,20 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
namespace App\EventListener;
use App\Entity\DrinkType;
use App\Entity\PropertyChangeLog;
use App\Service\DrinkTypeUpdate;
use App\Service\DrinkType\DrinkTypeUpdateLog;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Events;
use Doctrine\Persistence\Event\LifecycleEventArgs;
#[AsDoctrineListener(event: Events::postPersist)]
#[AsDoctrineListener(event: Events::postUpdate)]
final class PostPersistUpdateListener
final readonly class PostPersistUpdateListener
{
public function __construct(private DrinkTypeUpdate $drinkTypeUpdate) {}
public function __construct(private DrinkTypeUpdateLog $drinkTypeUpdate) {}
public function postPersist(LifecycleEventArgs $args): void
{
$this->log(Events::postPersist, $args);
@ -25,12 +24,12 @@ final class PostPersistUpdateListener
{
$this->log(Events::postUpdate, $args);
}
private function log(string $action, LifecycleEventArgs $args): void
{
$entity = $args->getObject();
match($entity::class) {
DrinkType::class => $this->drinkTypeUpdate->handleLog($action, $entity, $args->getObjectManager()),
default => null,
};
}
private function log(string $action, LifecycleEventArgs $args): void
{
$entity = $args->getObject();
match ($entity::class) {
DrinkType::class => $this->drinkTypeUpdate->handleLog($action, $entity, $args->getObjectManager()),
default => null,
};
}
}

View file

@ -25,20 +25,20 @@ class DrinkTypeRepository extends AbstractRepository
return parent::findBy(
criteria: [],
orderBy: [
'desiredStock' => 'DESC',
'wantedStock' => 'DESC',
],
);
}
/** @return DrinkType[] */
public function findDesired(): array
public function findWanted(): array
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->select('d')
->from(DrinkType::class, 'd')
->where('d.desiredStock > 0')
->orderBy('d.desiredStock', 'DESC')
->where('d.wantedStock > 0')
->orderBy('d.wantedStock', 'DESC')
->addOrderBy('d.name', 'ASC');
/** @var array<int, DrinkType> $result */

View file

@ -26,7 +26,7 @@ class SystemConfigRepository extends AbstractRepository
if (!($config instanceof SystemConfig)) {
$config = new SystemConfig();
$config->setKey($key);
$config->setValue($key->defaultValue($key));
$config->setValue($key->defaultValue());
$this->save($config);
}
return $config;

View file

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Service\Config;
use App\Entity\SystemConfig;
use App\Enum\SystemSettingKey;
use App\Service\ConfigurationService;
use Stringable;

View file

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Service\Config;
use App\Entity\SystemConfig;
use App\Enum\SystemSettingKey;
use App\Service\ConfigurationService;
@ -16,11 +15,6 @@ final readonly class LowStockMultiplier
public function getValue(): float
{
$value = $this->configService->getConfigValue(
SystemSettingKey::STOCK_LOW_MULTIPLIER,
SystemConfig::DEFAULT_STOCK_LOW_MULTIPLIER,
);
return (float) $value;
return (float) $this->configService->get(SystemSettingKey::STOCK_LOW_MULTIPLIER);
}
}

View file

@ -4,10 +4,8 @@ declare(strict_types=1);
namespace App\Service;
use App\Entity\SystemConfig;
use App\Enum\SystemSettingKey;
use App\Repository\SystemConfigRepository;
use InvalidArgumentException;
readonly class ConfigurationService
{

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Service\DrinkType;
use App\Entity\DrinkType;
use App\Entity\Order;
use App\Entity\OrderItem;
use App\Repository\DrinkTypeRepository;
use Doctrine\Common\Collections\ArrayCollection;
final readonly class CreateNewOrderItems
{
public function __construct(
private DrinkTypeRepository $drinkTypeRepository,
) {}
public function __invoke(Order $order): void
{
new ArrayCollection($this->drinkTypeRepository->findWanted())->forAll(
fn (DrinkType $drinkType) => $order
->addOrderItem(
new OrderItem()
->setDrinkType($drinkType)
->setQuantity($drinkType->getWantedStock() - $drinkType->getCurrentStock())
),
);
}
}

View file

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace App\Service\DrinkType;
use App\Entity\DrinkType;
use App\Entity\PropertyChangeLog;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Events;
final class DrinkTypeUpdateLog
{
public function handleLog(string $action, DrinkType $entity, EntityManagerInterface $em): void
{
match ($action) {
Events::postPersist => $this->handleCreate($entity, $em),
Events::postUpdate => $this->handleUpdate($entity, $em),
};
}
private function handleCreate(DrinkType $entity, EntityManagerInterface $em): void
{
$this->logWanted($entity, $em);
$this->logCurrent($entity, $em);
}
private function logWanted(DrinkType $entity, EntityManagerInterface $em): void
{
$logWanted = new PropertyChangeLog();
$logWanted->setNewValue((string) $entity->getWantedStock());
$logWanted->setEntityClass(DrinkType::class);
$logWanted->setEntityId($entity->getId());
$logWanted->setPropertyName('wantedStock');
$em->persist($logWanted);
$em->flush();
}
private function logCurrent(DrinkType $entity, EntityManagerInterface $em): void
{
$logCurrent = new PropertyChangeLog();
$logCurrent->setNewValue((string) $entity->getCurrentStock());
$logCurrent->setEntityClass(DrinkType::class);
$logCurrent->setEntityId($entity->getId());
$logCurrent->setPropertyName('currentStock');
$em->persist($logCurrent);
$em->flush();
}
private function handleUpdate(DrinkType $entity, EntityManagerInterface $em): void
{
$uow = $em->getUnitOfWork();
$changeSet = $uow->getEntityChangeSet($entity);
if (isset($changeSet['wantedStock'])) {
$this->logWanted($entity, $em);
}
if (isset($changeSet['currentStock'])) {
$this->logCurrent($entity, $em);
}
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Service\DrinkType;
use App\Entity\DrinkType;
use App\Entity\PropertyChangeLog;
use App\Repository\PropertyChangeLogRepository;
final readonly class GetStockHistory
{
public function __construct(
private PropertyChangeLogRepository $propertyChangeLogRepository,
) {}
/**
* @param DrinkType $drinkType
* @return PropertyChangeLog[]
*/
public function __invoke(DrinkType $drinkType): array
{
if ($drinkType->getId() === null) {
return[];
}
return $this->propertyChangeLogRepository->findBy([
'entityClass' => DrinkType::class,
'propertyName' => 'currentStock',
'entityId' => $drinkType->getId(),
]);
}
}

View file

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Service\DrinkType;
use App\Entity\DrinkType;
use App\Entity\PropertyChangeLog;
use App\Repository\PropertyChangeLogRepository;
final readonly class GetWantedHistory
{
public function __construct(private PropertyChangeLogRepository $propertyChangeLogRepository) {}
/** @return PropertyChangeLog[] */
public function __invoke(DrinkType $drinkType): array
{
if ($drinkType->getId() === null) {
return[];
}
return $this->propertyChangeLogRepository->findBy([
'entityClass' => DrinkType::class,
'propertyName' => 'wantedStock',
'entityId' => $drinkType->getId(),
]);
}
}

View file

@ -1,61 +0,0 @@
<?php declare(strict_types=1);
namespace App\Service;
use App\Entity\DrinkType;
use App\Entity\PropertyChangeLog;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Events;
final class DrinkTypeUpdate
{
public function handleLog(string $action, DrinkType $entity, EntityManagerInterface $em): void
{
match ($action) {
Events::postPersist => $this->handleCreate($entity, $em),
Events::postUpdate => $this->handleUpdate($entity, $em),
};
}
private function handleCreate(DrinkType $entity, EntityManagerInterface $em): void
{
$this->logWanted($entity, $em);
$this->logCurrent($entity, $em);
}
private function logWanted(DrinkType $entity, EntityManagerInterface $em): void
{
$logWanted = new PropertyChangeLog();
$logWanted->setNewValue((string) $entity->getWantedStock());
$logWanted->setEntityClass(DrinkType::class);
$logWanted->setEntityId($entity->getId());
$logWanted->setPropertyName('wantedStock');
$em->persist($logWanted);
$em->flush();
}
private function logCurrent(DrinkType $entity, EntityManagerInterface $em): void
{
$logCurrent = new PropertyChangeLog();
$logCurrent->setNewValue((string) $entity->getCurrentStock());
$logCurrent->setEntityClass(DrinkType::class);
$logCurrent->setEntityId($entity->getId());
$logCurrent->setPropertyName('currentStock');
$em->persist($logCurrent);
$em->flush();
}
private function handleUpdate(DrinkType $entity, EntityManagerInterface $em): void
{
$uow = $em->getUnitOfWork();
$changeSet = $uow->getEntityChangeSet($entity);
if (isset($changeSet['wantedStock'])) {
$this->logWanted($entity, $em);;
}
if (isset($changeSet['currentStock'])) {
$this->logCurrent($entity,$em);
}
}
}

View file

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Service;
use App\Entity\DrinkType;
use App\Entity\Order;
use App\Entity\OrderItem;
use App\Enum\OrderStatus;
@ -132,111 +131,4 @@ readonly class OrderService
return $order;
}
/**
* Create an order based on current stock levels
*
* @return Order
*/
public function createOrderFromStockLevels(): Order
{
$lowStockItems = $this->inventoryService->getAllDrinkTypesWithStockLevels();
$orderItems = [];
foreach ($lowStockItems as $item) {
if ($item->record->getQuantity() < $item->record->getDrinkType()->getDesiredStock()) {
$orderItems[] = [
'drinkTypeId' => $item->record->getDrinkType()->getId(),
'quantity' => $item->record->getDrinkType()->getDesiredStock() - $item->record->getQuantity(),
];
}
}
return $this->createOrder($orderItems);
}
/**
* Update an order's status
*
* @return Order
* @throws InvalidArgumentException If the status is invalid
*/
public function updateOrderStatus(Order $order, OrderStatus $status): Order
{
$order->setStatus($status);
$this->orderRepository->save($order);
return $order;
}
/**
* Add an item to an order
*
* @param Order $order
* @param DrinkType $drinkType
* @param int $quantity
* @return OrderItem
* @throws InvalidArgumentException If the order is not in 'new' or 'in_work' status
*/
public function addOrderItem(Order $order, DrinkType $drinkType, int $quantity): OrderItem
{
if (!in_array($order->getStatus(), [OrderStatus::NEW, OrderStatus::IN_WORK], true)) {
throw new InvalidArgumentException(
"Cannot add items to an order with status '{$order->getStatus()->value}'",
);
}
// Check if the order already has an item for this drink type
$existingItem = $this->orderItemRepository->findByOrderAndDrinkType($order, $drinkType);
if ($existingItem instanceof OrderItem) {
// Update the existing item
$existingItem->setQuantity($existingItem->getQuantity() + $quantity);
$this->orderItemRepository->save($existingItem);
return $existingItem;
}
// Create a new item
$orderItem = new OrderItem();
$orderItem->setQuantity($quantity);
$orderItem->setOrder($order);
$orderItem->setDrinkType($drinkType);
$this->orderItemRepository->save($orderItem);
return $orderItem;
}
/**
* Remove an item from an order
*
* @param Order $order
* @param OrderItem $orderItem
* @return void
* @throws InvalidArgumentException If the order is not in 'new' or 'in_work' status
*/
public function removeOrderItem(Order $order, OrderItem $orderItem): void
{
if (!in_array($order->getStatus(), [OrderStatus::NEW, OrderStatus::IN_WORK], true)) {
throw new InvalidArgumentException(
"Cannot remove items from an order with status '{$order->getStatus()->value}'",
);
}
$order->removeOrderItem($orderItem);
$this->orderItemRepository->remove($orderItem);
}
/**
* Delete an order
*
* @param Order $order
* @return void
* @throws InvalidArgumentException If the order is not in 'new' status
*/
public function deleteOrder(Order $order): void
{
if ($order->getStatus() !== OrderStatus::NEW && $order->getStatus() !== OrderStatus::IN_WORK) {
throw new InvalidArgumentException("Cannot delete an order with status '{$order->getStatus()->value}'");
}
$this->orderRepository->remove($order);
}
}

View file

@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace App\ValueObject;
use App\Entity\InventoryRecord;
use App\Enum\StockState;
final readonly class DrinkStock
{
public function __construct(
public InventoryRecord $record,
public StockState $stock,
) {}
public static function fromInventoryRecord(InventoryRecord $record, float $lowStockMultiplier): self
{
if ($record->getQuantity() === 0 && $record->getDrinkType()->getDesiredStock() > 0) {
return new self($record, StockState::CRITICAL);
}
if ($record->getQuantity() < ($record->getDrinkType()->getDesiredStock() * $lowStockMultiplier)) {
return new self($record, StockState::LOW);
}
if ($record->getQuantity() > $record->getDrinkType()->getDesiredStock()) {
return new self($record, StockState::HIGH);
}
return new self($record, StockState::NORMAL);
}
}