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

@ -25,6 +25,9 @@
"require-dev": {
"pestphp/pest": "^3.8",
"rector/rector": "^2.0",
"symfony/browser-kit": "^7.3",
"symfony/css-selector": "7.3.*",
"symfony/dom-crawler": "7.3.*",
"symfony/maker-bundle": "^1.63",
"symfony/stopwatch": "7.3.*",
"symfony/web-profiler-bundle": "7.3.*",

269
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "139ab419de1a7d3014e5595dbe864775",
"content-hash": "7dbc573cc9b3d6ab3fb532611e469c70",
"packages": [
{
"name": "doctrine/cache",
@ -5196,6 +5196,73 @@
},
"time": "2025-03-19T14:43:43+00:00"
},
{
"name": "masterminds/html5",
"version": "2.9.0",
"source": {
"type": "git",
"url": "https://github.com/Masterminds/html5-php.git",
"reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6",
"reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6",
"shasum": ""
},
"require": {
"ext-dom": "*",
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.7-dev"
}
},
"autoload": {
"psr-4": {
"Masterminds\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Matt Butcher",
"email": "technosophos@gmail.com"
},
{
"name": "Matt Farina",
"email": "matt@mattfarina.com"
},
{
"name": "Asmir Mustafic",
"email": "goetas@gmail.com"
}
],
"description": "An HTML5 parser and serializer.",
"homepage": "http://masterminds.github.io/html5-php",
"keywords": [
"HTML5",
"dom",
"html",
"parser",
"querypath",
"serializer",
"xml"
],
"support": {
"issues": "https://github.com/Masterminds/html5-php/issues",
"source": "https://github.com/Masterminds/html5-php/tree/2.9.0"
},
"time": "2024-03-31T07:05:07+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.13.1",
@ -7746,6 +7813,206 @@
],
"time": "2024-10-20T05:08:20+00:00"
},
{
"name": "symfony/browser-kit",
"version": "v7.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/browser-kit.git",
"reference": "5384291845e74fd7d54f3d925c4a86ce12336593"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/browser-kit/zipball/5384291845e74fd7d54f3d925c4a86ce12336593",
"reference": "5384291845e74fd7d54f3d925c4a86ce12336593",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/dom-crawler": "^6.4|^7.0"
},
"require-dev": {
"symfony/css-selector": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0",
"symfony/mime": "^6.4|^7.0",
"symfony/process": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\BrowserKit\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/browser-kit/tree/v7.3.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-03-05T10:15:41+00:00"
},
{
"name": "symfony/css-selector",
"version": "v7.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2",
"reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2",
"shasum": ""
},
"require": {
"php": ">=8.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\CssSelector\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Jean-François Simon",
"email": "jeanfrancois.simon@sensiolabs.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/css-selector/tree/v7.3.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/dom-crawler",
"version": "v7.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
"reference": "0fabbc3d6a9c473b716a93fc8e7a537adb396166"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/0fabbc3d6a9c473b716a93fc8e7a537adb396166",
"reference": "0fabbc3d6a9c473b716a93fc8e7a537adb396166",
"shasum": ""
},
"require": {
"masterminds/html5": "^2.6",
"php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.0"
},
"require-dev": {
"symfony/css-selector": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\DomCrawler\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Eases DOM navigation for HTML and XML documents",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/dom-crawler/tree/v7.3.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-03-05T10:15:41+00:00"
},
{
"name": "symfony/maker-bundle",
"version": "v1.63.0",

View file

@ -1,8 +1,6 @@
<?php
declare(strict_types=1);
use Doctrine\ORM\Events;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {

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);

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);
}
}

View file

@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
use App\Entity\DrinkType;
use App\Entity\PropertyChangeLog;
use Doctrine\ORM\EntityManagerInterface;
test('Update Listener', function () {
test('Update Listener', function (): void {
$drinkType = new DrinkType();
$drinkType->setName('test');
$drinkType->setWantedStock(10);
@ -16,26 +18,34 @@ test('Update Listener', function () {
$em->flush();
$propertyLogRepository = $em->getRepository(PropertyChangeLog::class);
$logs = $propertyLogRepository->findBy(['entityClass' => DrinkType::class]);
$logs = $propertyLogRepository->findBy([
'entityClass' => DrinkType::class,
]);
expect($logs)->toHaveCount(2);
$drinkType->setWantedStock(15);
$em->persist($drinkType);
$em->flush();
$logs = $propertyLogRepository->findBy(['entityClass' => DrinkType::class]);
$logs = $propertyLogRepository->findBy([
'entityClass' => DrinkType::class,
]);
expect($logs)->toHaveCount(3);
$drinkType->setCurrentStock(15);
$em->persist($drinkType);
$em->flush();
$logs = $propertyLogRepository->findBy(['entityClass' => DrinkType::class]);
$logs = $propertyLogRepository->findBy([
'entityClass' => DrinkType::class,
]);
expect($logs)->toHaveCount(4);
$drinkType->setDescription('test');
$em->persist($drinkType);
$em->flush();
$logs = $propertyLogRepository->findBy(['entityClass' => DrinkType::class]);
$logs = $propertyLogRepository->findBy([
'entityClass' => DrinkType::class,
]);
expect($logs)->toHaveCount(4);
});

View file

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
use App\Entity\DrinkType;
use App\Repository\DrinkTypeRepository;
use Doctrine\ORM\EntityManagerInterface;
test('findAll returns all drink types ordered by wantedStock DESC', function (): void {
$em = $this->getContainer()->get(EntityManagerInterface::class);
$repository = $this->getContainer()->get(DrinkTypeRepository::class);
// Clear existing drink types
$existingDrinkTypes = $repository->findAll();
foreach ($existingDrinkTypes as $drinkType) {
$em->remove($drinkType);
}
$em->flush();
// Create test drink types with different wantedStock values
$drinkType1 = new DrinkType();
$drinkType1->setName('Drink Type 1');
$drinkType1->setWantedStock(5);
$drinkType2 = new DrinkType();
$drinkType2->setName('Drink Type 2');
$drinkType2->setWantedStock(10);
$drinkType3 = new DrinkType();
$drinkType3->setName('Drink Type 3');
$drinkType3->setWantedStock(0);
// Persist the drink types
$em->persist($drinkType1);
$em->persist($drinkType2);
$em->persist($drinkType3);
$em->flush();
// Test findAll method
$result = $repository->findAll();
// Verify the result
expect($result)->toHaveCount(3);
expect($result[0]->getName())->toBe('Drink Type 2');
expect($result[1]->getName())->toBe('Drink Type 1');
expect($result[2]->getName())->toBe('Drink Type 3');
});
test('findWanted returns only drink types with wantedStock > 0 ordered by wantedStock DESC and name ASC', function (): void {
$em = $this->getContainer()->get(EntityManagerInterface::class);
$repository = $this->getContainer()->get(DrinkTypeRepository::class);
// Clear existing drink types
$existingDrinkTypes = $repository->findAll();
foreach ($existingDrinkTypes as $drinkType) {
$em->remove($drinkType);
}
$em->flush();
// Create test drink types with different wantedStock values
$drinkType1 = new DrinkType();
$drinkType1->setName('Cola');
$drinkType1->setWantedStock(10);
$drinkType2 = new DrinkType();
$drinkType2->setName('Beer');
$drinkType2->setWantedStock(10);
$drinkType3 = new DrinkType();
$drinkType3->setName('Water');
$drinkType3->setWantedStock(5);
$drinkType4 = new DrinkType();
$drinkType4->setName('Juice');
$drinkType4->setWantedStock(0);
// Persist the drink types
$em->persist($drinkType1);
$em->persist($drinkType2);
$em->persist($drinkType3);
$em->persist($drinkType4);
$em->flush();
// Test findWanted method
$result = $repository->findWanted();
// Verify the result
expect($result)->toHaveCount(3);
// First should be Beer (wantedStock 10, name starts with B)
expect($result[0]->getName())->toBe('Beer');
expect($result[0]->getWantedStock())->toBe(10);
// Second should be Cola (wantedStock 10, name starts with C)
expect($result[1]->getName())->toBe('Cola');
expect($result[1]->getWantedStock())->toBe(10);
// Third should be Water (wantedStock 5)
expect($result[2]->getName())->toBe('Water');
expect($result[2]->getWantedStock())->toBe(5);
// Juice should not be in the result (wantedStock 0)
$names = array_map(fn($dt) => $dt->getName(), $result);
expect($names)->not->toContain('Juice');
});

View file

@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
use App\Enum\SystemSettingKey;
use App\Service\Config\AppName;
use App\Service\ConfigurationService;
test('it returns the system name from configuration service', function () {
test('it returns the system name from configuration service', function (): void {
/** @var ConfigurationService $configService */
$configService = $this->getContainer()->get(ConfigurationService::class);

View file

@ -1,12 +1,9 @@
<?php
declare(strict_types=1);
use App\Entity\SystemConfig;
use App\Enum\SystemSettingKey;
use App\Service\ConfigurationService;
test('get returns correct value', function (): void {
// Arrange
/** @var ConfigurationService $configService */

View file

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
use App\Entity\DrinkType;
use App\Entity\PropertyChangeLog;
use App\Service\DrinkType\GetStockHistory;
use Doctrine\ORM\EntityManagerInterface;
test('it returns empty array for unsaved drink type', function (): void {
$drinkType = new DrinkType();
$drinkType->setName('Test Drink');
$getStockHistory = $this->getContainer()->get(GetStockHistory::class);
$result = $getStockHistory($drinkType);
expect($result)->toBeArray();
expect($result)->toBeEmpty();
});
test('it returns stock history for a drink type', function (): void {
// Create a drink type
$drinkType = new DrinkType();
$drinkType->setName('Test Drink');
$drinkType->setCurrentStock(10);
$em = $this->getContainer()->get(EntityManagerInterface::class);
$em->persist($drinkType);
$em->flush();
// Change the current stock to create logs
$drinkType->setCurrentStock(15);
$em->persist($drinkType);
$em->flush();
$drinkType->setCurrentStock(5);
$em->persist($drinkType);
$em->flush();
// Get the stock history
$getStockHistory = $this->getContainer()->get(GetStockHistory::class);
$result = $getStockHistory($drinkType);
// Verify the result
expect($result)->toBeArray();
expect($result)->toHaveCount(3);
// Verify that all logs are for currentStock
foreach ($result as $log) {
expect($log)->toBeInstanceOf(PropertyChangeLog::class);
expect($log->getPropertyName())->toBe('currentStock');
expect($log->getEntityClass())->toBe(DrinkType::class);
expect($log->getEntityId())->toBe($drinkType->getId());
}
// Verify the values in order (oldest first)
expect($result[0]->getNewValue())->toBe('10');
expect($result[1]->getNewValue())->toBe('15');
expect($result[2]->getNewValue())->toBe('5');
});
test('it only returns logs for the specified drink type', function (): void {
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create first drink type
$drinkType1 = new DrinkType();
$drinkType1->setName('Drink 1');
$drinkType1->setCurrentStock(10);
$em->persist($drinkType1);
$em->flush();
// Create second drink type
$drinkType2 = new DrinkType();
$drinkType2->setName('Drink 2');
$drinkType2->setCurrentStock(20);
$em->persist($drinkType2);
$em->flush();
// Get history for first drink type
$getStockHistory = $this->getContainer()->get(GetStockHistory::class);
$result = $getStockHistory($drinkType1);
// Verify we only get logs for the first drink type
expect($result)->toHaveCount(1);
expect($result[0]->getEntityId())->toBe($drinkType1->getId());
expect($result[0]->getNewValue())->toBe('10');
});

View file

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
use App\Entity\DrinkType;
use App\Entity\PropertyChangeLog;
use App\Service\DrinkType\GetWantedHistory;
use Doctrine\ORM\EntityManagerInterface;
test('it returns empty array for unsaved drink type', function (): void {
$drinkType = new DrinkType();
$drinkType->setName('Test Drink');
$getWantedHistory = $this->getContainer()->get(GetWantedHistory::class);
$result = $getWantedHistory($drinkType);
expect($result)->toBeArray();
expect($result)->toBeEmpty();
});
test('it returns wanted stock history for a drink type', function (): void {
// Create a drink type
$drinkType = new DrinkType();
$drinkType->setName('Test Drink');
$drinkType->setWantedStock(10);
$em = $this->getContainer()->get(EntityManagerInterface::class);
$em->persist($drinkType);
$em->flush();
// Change the wanted stock to create logs
$drinkType->setWantedStock(15);
$em->persist($drinkType);
$em->flush();
$drinkType->setWantedStock(5);
$em->persist($drinkType);
$em->flush();
// Get the wanted stock history
$getWantedHistory = $this->getContainer()->get(GetWantedHistory::class);
$result = $getWantedHistory($drinkType);
// Verify the result
expect($result)->toBeArray();
expect($result)->toHaveCount(3);
// Verify that all logs are for wantedStock
foreach ($result as $log) {
expect($log)->toBeInstanceOf(PropertyChangeLog::class);
expect($log->getPropertyName())->toBe('wantedStock');
expect($log->getEntityClass())->toBe(DrinkType::class);
expect($log->getEntityId())->toBe($drinkType->getId());
}
// Verify the values in order (oldest first)
expect($result[0]->getNewValue())->toBe('10');
expect($result[1]->getNewValue())->toBe('15');
expect($result[2]->getNewValue())->toBe('5');
});
test('it only returns logs for the specified drink type', function (): void {
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create first drink type
$drinkType1 = new DrinkType();
$drinkType1->setName('Drink 1');
$drinkType1->setWantedStock(10);
$em->persist($drinkType1);
$em->flush();
// Create second drink type
$drinkType2 = new DrinkType();
$drinkType2->setName('Drink 2');
$drinkType2->setWantedStock(20);
$em->persist($drinkType2);
$em->flush();
// Get history for first drink type
$getWantedHistory = $this->getContainer()->get(GetWantedHistory::class);
$result = $getWantedHistory($drinkType1);
// Verify we only get logs for the first drink type
expect($result)->toHaveCount(1);
expect($result[0]->getEntityId())->toBe($drinkType1->getId());
expect($result[0]->getNewValue())->toBe('10');
});

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
test('Hello World', function (): void {
// This calls KernelTestCase::bootKernel(), and creates a
// "client" that is acting as the browser
$this->ensureKernelShutdown();
$client = static::createClient();
// Request a specific page
$crawler = $client->request('GET', '/');
// Validate a successful response and some content
$this->assertResponseIsSuccessful();
$this->assertSelectorTextContains('h1', 'Hello World');
});

View file

@ -4,6 +4,6 @@ declare(strict_types=1);
namespace Tests;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
abstract class TestCase extends KernelTestCase {}
abstract class TestCase extends WebTestCase {}

View file

@ -1,7 +0,0 @@
<?php
declare(strict_types=1);
test('example', function (): void {
expect(true)->toBeTrue();
});