nochmal nue

This commit is contained in:
lubiana 2025-06-09 22:05:01 +02:00
parent 2c2e34b71e
commit 6f07d70436
Signed by: lubiana
SSH key fingerprint: SHA256:vW1EA0fRR3Fw+dD/sM0K+x3Il2gSry6YRYHqOeQwrfk
27 changed files with 311 additions and 1988 deletions

View file

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250609175837 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
CREATE TABLE drink_type (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, updated_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, name VARCHAR(255) NOT NULL, description CLOB DEFAULT NULL, desired_stock INTEGER NOT NULL)
SQL);
$this->addSql(<<<'SQL'
CREATE UNIQUE INDEX UNIQ_841484B15E237E06 ON drink_type (name)
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE inventory_record (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, drink_type_id INTEGER NOT NULL, quantity INTEGER NOT NULL, timestamp DATETIME NOT NULL --(DC2Type:datetime_immutable)
, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, updated_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, CONSTRAINT FK_9BE8033AE7E8D8A1 FOREIGN KEY (drink_type_id) REFERENCES drink_type (id) NOT DEFERRABLE INITIALLY IMMEDIATE)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_9BE8033AE7E8D8A1 ON inventory_record (drink_type_id)
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE "order" (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, updated_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, status VARCHAR(255) DEFAULT 'new' NOT NULL)
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE order_item (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, drink_type_id INTEGER NOT NULL, order_id INTEGER NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, updated_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, quantity INTEGER NOT NULL, CONSTRAINT FK_52EA1F09E7E8D8A1 FOREIGN KEY (drink_type_id) REFERENCES drink_type (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_52EA1F098D9F6D38 FOREIGN KEY (order_id) REFERENCES "order" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_52EA1F09E7E8D8A1 ON order_item (drink_type_id)
SQL);
$this->addSql(<<<'SQL'
CREATE INDEX IDX_52EA1F098D9F6D38 ON order_item (order_id)
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE property_change_log (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, property_name VARCHAR(255) NOT NULL, entity_class VARCHAR(255) NOT NULL, entity_id INTEGER DEFAULT NULL, new_value VARCHAR(255) NOT NULL, change_date DATETIME NOT NULL --(DC2Type:datetime_immutable)
)
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE system_config (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, updated_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
, "key" VARCHAR(255) NOT NULL, value CLOB NOT NULL)
SQL);
$this->addSql(<<<'SQL'
CREATE UNIQUE INDEX UNIQ_C4049ABD8A90ABA9 ON system_config ("key")
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
DROP TABLE drink_type
SQL);
$this->addSql(<<<'SQL'
DROP TABLE inventory_record
SQL);
$this->addSql(<<<'SQL'
DROP TABLE "order"
SQL);
$this->addSql(<<<'SQL'
DROP TABLE order_item
SQL);
$this->addSql(<<<'SQL'
DROP TABLE property_change_log
SQL);
$this->addSql(<<<'SQL'
DROP TABLE system_config
SQL);
}
}

View file

@ -14,9 +14,6 @@ use Symfony\Component\Routing\Attribute\Route;
#[Route(path: '/', name: 'app_index')] #[Route(path: '/', name: 'app_index')]
final class Index extends AbstractController final class Index extends AbstractController
{ {
public function __construct(
private readonly InventoryService $inventoryService,
) {}
public function __invoke(): Response public function __invoke(): Response
{ {

View file

@ -9,7 +9,6 @@ use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use App\Entity\PropertyChangeLog;
#[ORM\Entity(repositoryClass: DrinkTypeRepository::class)] #[ORM\Entity(repositoryClass: DrinkTypeRepository::class)]
#[ORM\Table(name: 'drink_type')] #[ORM\Table(name: 'drink_type')]
@ -26,9 +25,6 @@ class DrinkType
#[ORM\Column(type: 'datetime_immutable')] #[ORM\Column(type: 'datetime_immutable')]
private DateTimeImmutable $updatedAt; private DateTimeImmutable $updatedAt;
#[ORM\OneToMany(mappedBy: 'drinkType', targetEntity: InventoryRecord::class)]
private Collection $inventoryRecords;
#[ORM\OneToMany(mappedBy: 'drinkType', targetEntity: OrderItem::class)] #[ORM\OneToMany(mappedBy: 'drinkType', targetEntity: OrderItem::class)]
private Collection $orderItems; private Collection $orderItems;
@ -37,16 +33,29 @@ class DrinkType
#[ORM\Column(type: 'text', nullable: true)] #[ORM\Column(type: 'text', nullable: true)]
private null|string $description = null; private null|string $description = null;
#[ORM\Column(type: 'integer')] #[ORM\Column(type: 'integer')]
private int $desiredStock = 10; private int $wantedStock = 10;
#[ORM\Column(type: 'integer')]
private int $currentStock = 0;
public function __construct( public function __construct(
) { ) {
$this->createdAt = new DateTimeImmutable(); $this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable(); $this->updatedAt = new DateTimeImmutable();
$this->inventoryRecords = new ArrayCollection();
$this->orderItems = new ArrayCollection(); $this->orderItems = new ArrayCollection();
} }
public function getCurrentStock(): int
{
return $this->currentStock;
}
public function setCurrentStock(int $currentStock): void
{
$this->currentStock = $currentStock;
}
public function getId(): null|int public function getId(): null|int
{ {
return $this->id; return $this->id;
@ -60,7 +69,6 @@ class DrinkType
public function setName(string $name): self public function setName(string $name): self
{ {
$this->name = $name; $this->name = $name;
$this->updateTimestamp();
return $this; return $this;
} }
@ -72,22 +80,21 @@ class DrinkType
public function setDescription(null|string $description): self public function setDescription(null|string $description): self
{ {
$this->description = $description; $this->description = $description;
$this->updateTimestamp();
return $this; return $this;
} }
public function getDesiredStock(): int public function getWantedStock(): int
{ {
return $this->desiredStock; return $this->wantedStock;
} }
public function setDesiredStock(int $desiredStock): self public function setWantedStock(int $wantedStock): self
{ {
$this->desiredStock = $desiredStock; $this->wantedStock = $wantedStock;
$this->updateTimestamp();
return $this; return $this;
} }
public function getCreatedAt(): DateTimeImmutable public function getCreatedAt(): DateTimeImmutable
{ {
return $this->createdAt; return $this->createdAt;
@ -98,16 +105,12 @@ class DrinkType
return $this->updatedAt; return $this->updatedAt;
} }
public function getInventoryRecords(): Collection
{
return $this->inventoryRecords;
}
public function getOrderItems(): Collection public function getOrderItems(): Collection
{ {
return $this->orderItems; return $this->orderItems;
} }
#[ORM\PrePersist]
private function updateTimestamp(): void private function updateTimestamp(): void
{ {
$this->updatedAt = new DateTimeImmutable(); $this->updatedAt = new DateTimeImmutable();

View file

@ -1,98 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Entity;
use App\Repository\InventoryRecordRepository;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: InventoryRecordRepository::class)]
#[ORM\Table(name: 'inventory_record')]
class InventoryRecord
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private null|int $id = null;
#[ORM\ManyToOne(targetEntity: DrinkType::class, inversedBy: 'inventoryRecords')]
#[ORM\JoinColumn(name: 'drink_type_id', referencedColumnName: 'id', nullable: false)]
private DrinkType $drinkType;
#[ORM\Column(type: 'integer')]
private int $quantity;
#[ORM\Column(type: 'datetime_immutable')]
private DateTimeImmutable $timestamp;
#[ORM\Column(type: 'datetime_immutable')]
private DateTimeImmutable $createdAt;
#[ORM\Column(type: 'datetime_immutable')]
private DateTimeImmutable $updatedAt;
public function __construct(
) {
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
$this->timestamp = new DateTimeImmutable();
}
public function getId(): null|int
{
return $this->id;
}
public function getDrinkType(): DrinkType
{
return $this->drinkType;
}
public function setDrinkType(DrinkType $drinkType): self
{
$this->drinkType = $drinkType;
$this->updateTimestamp();
return $this;
}
public function getQuantity(): int
{
return $this->quantity;
}
public function setQuantity(int $quantity): self
{
$this->quantity = $quantity;
$this->updateTimestamp();
return $this;
}
public function getTimestamp(): DateTimeImmutable
{
return $this->timestamp;
}
public function setTimestamp(DateTimeImmutable $timestamp): self
{
$this->timestamp = $timestamp;
$this->updateTimestamp();
return $this;
}
public function getCreatedAt(): DateTimeImmutable
{
return $this->createdAt;
}
public function getUpdatedAt(): DateTimeImmutable
{
return $this->updatedAt;
}
private function updateTimestamp(): void
{
$this->updatedAt = new DateTimeImmutable();
}
}

View file

@ -14,7 +14,7 @@ use Doctrine\ORM\Mapping as ORM;
class SystemConfig class SystemConfig
{ {
public const string DEFAULT_STOCK_ADJUSTMENT_LOOKBACK_ORDERS = '3'; public const string DEFAULT_STOCK_ADJUSTMENT_LOOKBACK_ORDERS = '3';
public const string DEFAULT_DEFAULT_DESIRED_STOCK = '2'; public const string DEFAULT_DEFAULT_WANTED_STOCK = '2';
public const string DEFAULT_SYSTEM_NAME = 'Zaufen'; public const string DEFAULT_SYSTEM_NAME = 'Zaufen';
public const string DEFAULT_STOCK_INCREASE_AMOUNT = '1'; public const string DEFAULT_STOCK_INCREASE_AMOUNT = '1';
public const string DEFAULT_STOCK_DECREASE_AMOUNT = '1'; public const string DEFAULT_STOCK_DECREASE_AMOUNT = '1';
@ -56,7 +56,6 @@ class SystemConfig
public function setKey(SystemSettingKey $key): self public function setKey(SystemSettingKey $key): self
{ {
$this->key = $key; $this->key = $key;
$this->updateTimestamp();
return $this; return $this;
} }
@ -68,7 +67,6 @@ class SystemConfig
public function setValue(string $value): self public function setValue(string $value): self
{ {
$this->value = $value; $this->value = $value;
$this->updateTimestamp();
return $this; return $this;
} }
@ -82,7 +80,8 @@ class SystemConfig
return $this->updatedAt; return $this->updatedAt;
} }
private function updateTimestamp(): void #[ORM\PrePersist]
protected function updateTimestamp(): void
{ {
$this->updatedAt = new DateTimeImmutable(); $this->updatedAt = new DateTimeImmutable();
} }

View file

@ -12,17 +12,17 @@ use App\Entity\SystemConfig;
enum SystemSettingKey: string enum SystemSettingKey: string
{ {
case STOCK_ADJUSTMENT_LOOKBACK_ORDERS = 'stock_adjustment_lookback_orders'; case STOCK_ADJUSTMENT_LOOKBACK_ORDERS = 'stock_adjustment_lookback_orders';
case DEFAULT_DESIRED_STOCK = 'default_desired_stock'; case DEFAULT_WANTED_STOCK = 'default_wanted_stock';
case SYSTEM_NAME = 'system_name'; case SYSTEM_NAME = 'system_name';
case STOCK_INCREASE_AMOUNT = 'stock_increase_amount'; case STOCK_INCREASE_AMOUNT = 'stock_increase_amount';
case STOCK_DECREASE_AMOUNT = 'stock_decrease_amount'; case STOCK_DECREASE_AMOUNT = 'stock_decrease_amount';
case STOCK_LOW_MULTIPLIER = 'stock_low_multiplier'; case STOCK_LOW_MULTIPLIER = 'stock_low_multiplier';
public static function getDefaultValue(self $key): string public function defaultValue(): string
{ {
return match ($key) { return match ($this) {
self::STOCK_ADJUSTMENT_LOOKBACK_ORDERS => SystemConfig::DEFAULT_STOCK_ADJUSTMENT_LOOKBACK_ORDERS, self::STOCK_ADJUSTMENT_LOOKBACK_ORDERS => SystemConfig::DEFAULT_STOCK_ADJUSTMENT_LOOKBACK_ORDERS,
self::DEFAULT_DESIRED_STOCK => SystemConfig::DEFAULT_DEFAULT_DESIRED_STOCK, self::DEFAULT_WANTED_STOCK => SystemConfig::DEFAULT_DEFAULT_WANTED_STOCK,
self::SYSTEM_NAME => SystemConfig::DEFAULT_SYSTEM_NAME, self::SYSTEM_NAME => SystemConfig::DEFAULT_SYSTEM_NAME,
self::STOCK_INCREASE_AMOUNT => SystemConfig::DEFAULT_STOCK_INCREASE_AMOUNT, self::STOCK_INCREASE_AMOUNT => SystemConfig::DEFAULT_STOCK_INCREASE_AMOUNT,
self::STOCK_DECREASE_AMOUNT => SystemConfig::DEFAULT_STOCK_DECREASE_AMOUNT, self::STOCK_DECREASE_AMOUNT => SystemConfig::DEFAULT_STOCK_DECREASE_AMOUNT,

View file

@ -0,0 +1,36 @@
<?php declare(strict_types=1);
namespace App\EventListener;
use App\Entity\DrinkType;
use App\Entity\PropertyChangeLog;
use App\Service\DrinkTypeUpdate;
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
{
public function __construct(private DrinkTypeUpdate $drinkTypeUpdate) {}
public function postPersist(LifecycleEventArgs $args): void
{
$this->log(Events::postPersist, $args);
}
public function postUpdate(LifecycleEventArgs $args): void
{
$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,
};
}
}

View file

@ -1,71 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Repository;
use App\Entity\DrinkType;
use App\Entity\InventoryRecord;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
/**
* @extends AbstractRepository<InventoryRecord>
*/
class InventoryRecordRepository extends AbstractRepository
{
public function __construct(EntityManagerInterface $entityManager)
{
parent::__construct($entityManager, $entityManager->getClassMetadata(InventoryRecord::class));
}
/**
* @return array<int, InventoryRecord>
*/
public function findByDrinkType(DrinkType $drinkType): array
{
return $this->findBy(
[
'drinkType' => $drinkType,
],
[
'timestamp' => 'DESC',
],
);
}
public function findLatestByDrinkType(DrinkType $drinkType): null|InventoryRecord
{
$records = $this->findBy(
[
'drinkType' => $drinkType,
],
[
'timestamp' => 'DESC',
],
1,
);
return $records[0] ?? null;
}
/**
* @return array<int, InventoryRecord>
*/
public function findByTimestampRange(DateTimeImmutable $start, DateTimeImmutable $end): array
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb
->select('ir')
->from(InventoryRecord::class, 'ir')
->where('ir.timestamp >= :start')
->andWhere('ir.timestamp <= :end')
->setParameter('start', $start)
->setParameter('end', $end)
->orderBy('ir.timestamp', 'DESC');
/** @var array<int, InventoryRecord> $result */
$result = $qb->getQuery()->getResult();
return $result;
}
}

View file

@ -26,29 +26,9 @@ class SystemConfigRepository extends AbstractRepository
if (!($config instanceof SystemConfig)) { if (!($config instanceof SystemConfig)) {
$config = new SystemConfig(); $config = new SystemConfig();
$config->setKey($key); $config->setKey($key);
$config->setValue($key->getDefaultValue($key)); $config->setValue($key->defaultValue($key));
$this->save($config); $this->save($config);
} }
return $config; return $config;
} }
public function getValue(SystemSettingKey $key, string $default = ''): string
{
$config = $this->findByKey($key);
return ($config instanceof SystemConfig) ? $config->getValue() : $default;
}
public function setValue(SystemSettingKey $key, string $value): void
{
$config = $this->findByKey($key);
if ($config instanceof SystemConfig) {
$config->setValue($value);
} else {
$config = new SystemConfig();
$config->setKey($key);
$config->setValue($value);
}
$this->save($config);
}
} }

View file

@ -17,6 +17,6 @@ final readonly class AppName implements Stringable
public function __toString(): string public function __toString(): string
{ {
return $this->configService->getConfigValue(SystemSettingKey::SYSTEM_NAME, SystemConfig::DEFAULT_SYSTEM_NAME); return $this->configService->get(SystemSettingKey::SYSTEM_NAME);
} }
} }

View file

@ -15,64 +15,30 @@ readonly class ConfigurationService
private SystemConfigRepository $systemConfigRepository, private SystemConfigRepository $systemConfigRepository,
) {} ) {}
/** public function get(SystemSettingKey $key): string
* Get all configuration entries
*
* @return SystemConfig[]
*/
public function getAllConfigs(): array
{ {
return $this->systemConfigRepository->findAll(); return $this->systemConfigRepository->findByKey($key)->getValue();
} }
public function getConfigValue(SystemSettingKey $key, string $default = ''): string public function set(SystemSettingKey $key, string $value): void
{ {
return $this->systemConfigRepository->getValue($key, $default); $config = $this->systemConfigRepository->findByKey($key);
if ($config->getValue() === $value) {
return;
} }
public function setConfigValue(SystemSettingKey $key, string $value): void
{
$this->systemConfigRepository->setValue($key, $value);
}
public function getConfigByKey(SystemSettingKey $key): SystemConfig
{
return $this->systemConfigRepository->findByKey($key);
}
public function createConfig(SystemSettingKey $key, string $value): SystemConfig
{
if ($this->systemConfigRepository->findByKey($key) instanceof SystemConfig) {
throw new InvalidArgumentException("A configuration with the key '{$key->value}' already exists");
}
$config = new SystemConfig();
$config->setKey($key);
$config->setValue($value); $config->setValue($value);
$this->systemConfigRepository->save($config); $this->systemConfigRepository->save($config);
return $config;
} }
public function reset(SystemSettingKey $key): void
public function updateConfig(SystemConfig $config, string $value): SystemConfig
{ {
if ($value !== '') { $this->set($key, $key->defaultValue());
$config->setValue($value);
}
$this->systemConfigRepository->save($config);
return $config;
} }
public function resetAllConfigs(): void public function resetAll(): void
{ {
foreach (SystemSettingKey::cases() as $key) { foreach (SystemSettingKey::cases() as $key) {
$this->setDefaultValue($key); $this->reset($key);
} }
} }
public function setDefaultValue(SystemSettingKey $key): void
{
$this->setConfigValue($key, SystemSettingKey::getDefaultValue($key));
}
} }

View file

@ -1,144 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\DrinkType;
use App\Enum\SystemSettingKey;
use App\Repository\DrinkTypeRepository;
use InvalidArgumentException;
readonly class DrinkTypeService
{
public function __construct(
private DrinkTypeRepository $drinkTypeRepository,
private ConfigurationService $configService,
) {}
/**
* Get all drink types
*
* @return DrinkType[]
*/
public function getAllDrinkTypes(): array
{
return $this->drinkTypeRepository->findAll();
}
/**
* Get a drink type by ID
*
* @param int $id
* @return DrinkType|null
*/
public function getDrinkTypeById(int $id): null|DrinkType
{
return $this->drinkTypeRepository->find($id);
}
/**
* Get a drink type by name
*
* @param string $name
* @return DrinkType|null
*/
public function getDrinkTypeByName(string $name): null|DrinkType
{
return $this->drinkTypeRepository->findOneBy([
'name' => $name,
]);
}
/**
* Create a new drink type
*
* @param string $name
* @param string|null $description
* @param int|null $desiredStock
* @return DrinkType
* @throws InvalidArgumentException If a drink type with the same name already exists
*/
public function createDrinkType(
string $name,
null|string $description = null,
null|int $desiredStock = null,
): DrinkType {
// Check if a drink type with the same name already exists
if (
$this->drinkTypeRepository->findOneBy([
'name' => $name,
]) !== null
) {
throw new InvalidArgumentException("A drink type with the name '{$name}' already exists");
}
// If no desired stock is provided, use the default from configuration
if ($desiredStock === null) {
$desiredStock = (int) $this->configService->getConfigByKey(SystemSettingKey::DEFAULT_DESIRED_STOCK)->getValue();
}
$drinkType = new DrinkType();
$drinkType->setName($name);
$drinkType->setDescription($description);
$drinkType->setDesiredStock($desiredStock);
$this->drinkTypeRepository->save($drinkType);
return $drinkType;
}
/**
* Update an existing drink type
*
* @param DrinkType $drinkType
* @param string|null $name
* @param string|null $description
* @param int|null $desiredStock
* @return DrinkType
* @throws InvalidArgumentException If a drink type with the same name already exists
*/
public function updateDrinkType(
DrinkType $drinkType,
null|string $name = null,
null|string $description = null,
null|int $desiredStock = null,
): DrinkType {
// Update name if provided
if ($name !== null && $name !== $drinkType->getName()) {
// Check if a drink type with the same name already exists
if (
$this->drinkTypeRepository->findOneBy([
'name' => $name,
]) !== null
) {
throw new InvalidArgumentException("A drink type with the name '{$name}' already exists");
}
$drinkType->setName($name);
}
// Update description if provided
if ($description !== null) {
$drinkType->setDescription($description);
}
// Update desired stock if provided
if ($desiredStock !== null) {
$drinkType->setDesiredStock($desiredStock);
}
$this->drinkTypeRepository->save($drinkType);
return $drinkType;
}
/**
* Delete a drink type
*
* @param DrinkType $drinkType
* @return void
*/
public function deleteDrinkType(DrinkType $drinkType): void
{
$this->drinkTypeRepository->remove($drinkType);
}
}

View file

@ -0,0 +1,61 @@
<?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

@ -1,125 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\DrinkType;
use App\Entity\InventoryRecord;
use App\Repository\DrinkTypeRepository;
use App\Repository\InventoryRecordRepository;
use App\Service\Config\LowStockMultiplier;
use App\ValueObject\DrinkStock;
use DateTimeImmutable;
readonly class InventoryService
{
public function __construct(
private InventoryRecordRepository $inventoryRecordRepository,
private DrinkTypeRepository $drinkTypeRepository,
private LowStockMultiplier $lowStockMultiplier,
) {}
/**
* Get all inventory records
*
* @return InventoryRecord[]
*/
public function getAllInventoryRecords(): array
{
return $this->inventoryRecordRepository->findAll();
}
/**
* Get inventory records for a specific drink type
*
* @param DrinkType $drinkType
* @return InventoryRecord[]
*/
public function getInventoryRecordsByDrinkType(DrinkType $drinkType): array
{
return $this->inventoryRecordRepository->findByDrinkType($drinkType);
}
/**
* Get the latest inventory record for a specific drink type
*
* @param DrinkType $drinkType
* @return InventoryRecord
*/
public function getLatestInventoryRecord(DrinkType $drinkType): InventoryRecord
{
$record = $this->inventoryRecordRepository->findLatestByDrinkType($drinkType);
if (!($record instanceof InventoryRecord)) {
$record = new InventoryRecord();
$record->setDrinkType($drinkType);
$record->setQuantity(0);
$this->inventoryRecordRepository->save($record);
}
return $record;
}
/**
* Get the current stock level for a specific drink type
*
* @param DrinkType $drinkType
* @return int
*/
public function getCurrentStockLevel(DrinkType $drinkType): int
{
$latestRecord = $this->getLatestInventoryRecord($drinkType);
return ($latestRecord instanceof InventoryRecord) ? $latestRecord->getQuantity() : 0;
}
/**
* Update the stock level for a specific drink type
*
* @param DrinkType $drinkType
* @param int $quantity
* @param DateTimeImmutable|null $timestamp
* @return InventoryRecord
*/
public function updateStockLevel(
DrinkType $drinkType,
int $quantity,
DateTimeImmutable $timestamp = new DateTimeImmutable(),
): InventoryRecord {
$inventoryRecord = new InventoryRecord();
$inventoryRecord->setDrinkType($drinkType);
$inventoryRecord->setQuantity($quantity);
$inventoryRecord->setTimestamp($timestamp);
$this->inventoryRecordRepository->save($inventoryRecord);
return $inventoryRecord;
}
/**
* @return DrinkStock[]
*/
public function getAllDrinkTypesWithStockLevels(bool $includeZeroDesiredStock = false): array
{
if ($includeZeroDesiredStock) {
$drinkTypes = $this->drinkTypeRepository->findAll();
} else {
$drinkTypes = $this->drinkTypeRepository->findDesired();
}
$result = [];
foreach ($drinkTypes as $drinkType) {
$result[] = $this->getDrinkStock($drinkType);
}
return $result;
}
public function getDrinkStock(DrinkType $drinkType): DrinkStock
{
return DrinkStock::fromInventoryRecord(
$this->getLatestInventoryRecord($drinkType),
$this->lowStockMultiplier->getValue(),
);
}
}

View file

@ -20,7 +20,6 @@ readonly class OrderService
private OrderRepository $orderRepository, private OrderRepository $orderRepository,
private OrderItemRepository $orderItemRepository, private OrderItemRepository $orderItemRepository,
private DrinkTypeRepository $drinkTypeRepository, private DrinkTypeRepository $drinkTypeRepository,
private InventoryService $inventoryService,
) {} ) {}
/** /**

View file

@ -1,124 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\DrinkType;
use App\Enum\SystemSettingKey;
use App\Repository\DrinkTypeRepository;
use App\Repository\InventoryRecordRepository;
use App\Repository\OrderRepository;
use App\Repository\SystemConfigRepository;
use App\ValueObject\StockAdjustmentProposal;
readonly class StockAdjustmentService
{
public function __construct(
private DrinkTypeRepository $drinkTypeRepository,
private InventoryRecordRepository $inventoryRecordRepository,
private OrderRepository $orderRepository,
private SystemConfigRepository $systemConfigRepository,
) {}
/**
* Proposes adjusted stock levels for all drink types
*
* @return array<int, StockAdjustmentProposal> Array of stock adjustment proposals
*/
public function proposeStockAdjustments(): array
{
$drinkTypes = $this->drinkTypeRepository->findAll();
$proposals = [];
foreach ($drinkTypes as $drinkType) {
$proposals[] = $this->proposeStockAdjustmentForDrinkType($drinkType);
}
return $proposals;
}
/**
* Proposes an adjusted stock level for a specific drink type
*/
public function proposeStockAdjustmentForDrinkType(DrinkType $drinkType): StockAdjustmentProposal
{
$currentDesiredStock = $drinkType->getDesiredStock();
$lookbackOrders =
(int) $this->systemConfigRepository->getValue(SystemSettingKey::STOCK_ADJUSTMENT_LOOKBACK_ORDERS);
$increaseAmount = (int) $this->systemConfigRepository->getValue(SystemSettingKey::STOCK_INCREASE_AMOUNT);
$decreaseAmount = (int) $this->systemConfigRepository->getValue(SystemSettingKey::STOCK_DECREASE_AMOUNT);
// Get the last N orders for this drink type
$lastOrders = $this->orderRepository->findLastOrdersForDrinkType($drinkType, $lookbackOrders);
// If there are no orders, return the current desired stock
if ($lastOrders === []) {
return new StockAdjustmentProposal($drinkType, $currentDesiredStock);
}
// Check if stock was 0 in the last order
$lastOrder = $lastOrders[0];
$lastOrderItems = $lastOrder->getOrderItems();
$stockWasZeroInLastOrder = false;
foreach ($lastOrderItems as $orderItem) {
if ($orderItem->getDrinkType()->getId() === $drinkType->getId()) {
// Find the inventory record closest to the order creation date
$inventoryRecords = $this->inventoryRecordRepository->findByDrinkType($drinkType);
foreach ($inventoryRecords as $record) {
if ($record->getTimestamp() <= $lastOrder->getCreatedAt()) {
if ($record->getQuantity() === 0) {
$stockWasZeroInLastOrder = true;
}
break;
}
}
break;
}
}
// If stock was 0 in the last order, increase desired stock
if ($stockWasZeroInLastOrder) {
return new StockAdjustmentProposal($drinkType, $currentDesiredStock + $increaseAmount);
}
// Check if stock was above zero in all lookback orders
$stockWasAboveZeroInAllOrders = true;
$ordersToCheck = min(count($lastOrders), $lookbackOrders);
for ($i = 0; $i < $ordersToCheck; $i++) {
$order = $lastOrders[$i];
$orderItems = $order->getOrderItems();
foreach ($orderItems as $orderItem) {
if ($orderItem->getDrinkType()->getId() === $drinkType->getId()) {
// Find the inventory record closest to the order creation date
$inventoryRecords = $this->inventoryRecordRepository->findByDrinkType($drinkType);
foreach ($inventoryRecords as $record) {
if ($record->getTimestamp() <= $order->getCreatedAt()) {
if ($record->getQuantity() === 0) {
$stockWasAboveZeroInAllOrders = false;
}
break;
}
}
break;
}
}
if (!$stockWasAboveZeroInAllOrders) {
break;
}
}
// If stock was above zero in all lookback orders, decrease desired stock
if ($stockWasAboveZeroInAllOrders && $ordersToCheck === $lookbackOrders) {
$proposedStock = max(0, $currentDesiredStock - $decreaseAmount);
return new StockAdjustmentProposal($drinkType, $proposedStock);
}
// Otherwise, keep the current desired stock
return new StockAdjustmentProposal($drinkType, $currentDesiredStock);
}
}

View file

@ -124,7 +124,6 @@
<thead> <thead>
<tr> <tr>
<th>Date</th> <th>Date</th>
<th>Old Value</th>
<th>New Value</th> <th>New Value</th>
</tr> </tr>
</thead> </thead>
@ -132,7 +131,6 @@
{% for log in desired_stock_history %} {% for log in desired_stock_history %}
<tr> <tr>
<td>{{ log.changeDate|date('Y-m-d H:i:s') }}</td> <td>{{ log.changeDate|date('Y-m-d H:i:s') }}</td>
<td>{{ log.oldValue }}</td>
<td>{{ log.newValue }}</td> <td>{{ log.newValue }}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View file

@ -11,6 +11,16 @@ use Doctrine\ORM\Tools\SchemaTool;
abstract class DbTestCase extends TestCase abstract class DbTestCase extends TestCase
{ {
/**
* @template T of object
* @param class-string<T> $id
* @return object<T>
*/
protected function get(string $id): object
{
return $this->getContainer()->get($id);
}
protected function setUp(): void protected function setUp(): void
{ {
$em = $this->getContainer()->get(EntityManagerInterface::class); $em = $this->getContainer()->get(EntityManagerInterface::class);

View file

@ -1,48 +0,0 @@
<?php
declare(strict_types=1);
use App\Entity\DrinkType;
use App\Entity\PropertyChangeLog;
use App\Repository\PropertyChangeLogRepository;
use Doctrine\ORM\EntityManagerInterface;
test('property change log is created when drink type desired stock is updated', function (): void {
// Arrange
$em = $this->getContainer()->get(EntityManagerInterface::class);
$propertyChangeLogRepository = $this->getContainer()->get(PropertyChangeLogRepository::class);
// Create a drink type
$drinkType = new DrinkType();
$drinkType->setName('Test Drink Type');
$drinkType->setDescription('Test Description');
$drinkType->setDesiredStock(5);
$em->persist($drinkType);
$em->flush();
$drinkTypeId = $drinkType->getId();
// Act - Update the desired stock
$drinkType->setDesiredStock(10);
$em->flush();
// Manually create a PropertyChangeLog entry since the event listener might not work in tests
$log = new PropertyChangeLog();
$log->setEntityClass(DrinkType::class);
$log->setEntityId($drinkTypeId);
$log->setPropertyName('desiredStock');
$log->setNewValue('10');
$em->persist($log);
$em->flush();
// Assert - Check that a PropertyChangeLog entry was created
$logs = $propertyChangeLogRepository->findBy([
'entityClass' => DrinkType::class,
'propertyName' => 'desiredStock',
'entityId' => $drinkTypeId
], ['changeDate' => 'DESC']);
expect($logs)->toHaveCount(1);
expect($logs[0])->toBeInstanceOf(PropertyChangeLog::class);
expect($logs[0]->getNewValue())->toBe('10');
});

View file

@ -0,0 +1,41 @@
<?php
use App\Entity\DrinkType;
use App\Entity\PropertyChangeLog;
use Doctrine\ORM\EntityManagerInterface;
test('Update Listener', function () {
$drinkType = new DrinkType();
$drinkType->setName('test');
$drinkType->setWantedStock(10);
$drinkType->setCurrentStock(10);
$em = $this->get(EntityManagerInterface::class);
$em->persist($drinkType);
$em->flush();
$propertyLogRepository = $em->getRepository(PropertyChangeLog::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]);
expect($logs)->toHaveCount(3);
$drinkType->setCurrentStock(15);
$em->persist($drinkType);
$em->flush();
$logs = $propertyLogRepository->findBy(['entityClass' => DrinkType::class]);
expect($logs)->toHaveCount(4);
$drinkType->setDescription('test');
$em->persist($drinkType);
$em->flush();
$logs = $propertyLogRepository->findBy(['entityClass' => DrinkType::class]);
expect($logs)->toHaveCount(4);
});

View file

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
// tests/Feature/FeatureTestBootstrap.php
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
uses(KernelTestCase::class)->in(__DIR__);
beforeEach(function (): void {
$em = self::getContainer()->get(EntityManagerInterface::class);
createDatabaseSchema($em);
});
afterEach(function (): void {
$em = self::getContainer()->get(EntityManagerInterface::class);
deleteDatabaseFile($em);
});

View file

@ -0,0 +1,18 @@
<?php
use App\Enum\SystemSettingKey;
use App\Service\Config\AppName;
use App\Service\ConfigurationService;
test('it returns the system name from configuration service', function () {
/** @var ConfigurationService $configService */
$configService = $this->getContainer()->get(ConfigurationService::class);
$appName = new AppName($configService);
expect((string)$appName)->toBe(SystemSettingKey::SYSTEM_NAME->defaultValue());
$expected = 'Test System Name';
$configService->set(SystemSettingKey::SYSTEM_NAME, $expected);
expect((string)$appName)->toBe($expected);
});

View file

@ -1,88 +0,0 @@
<?php
declare(strict_types=1);
use App\Entity\SystemConfig;
use App\Enum\SystemSettingKey;
use App\Service\Config\AppName;
use App\Service\Config\LowStockMultiplier;
use App\Service\ConfigurationService;
test('AppName returns system name from configuration', function (): void {
// Arrange
$appName = $this->getContainer()->get(AppName::class);
$configService = $this->getContainer()->get(ConfigurationService::class);
$testSystemName = 'Test System Name';
// Set a custom system name
$configService->setConfigValue(SystemSettingKey::SYSTEM_NAME, $testSystemName);
// Act
$result = (string) $appName;
// Assert
expect($result)->toBe($testSystemName);
});
test('AppName returns default system name when not configured', function (): void {
// Arrange
$appName = $this->getContainer()->get(AppName::class);
$configService = $this->getContainer()->get(ConfigurationService::class);
// Reset to default value
$configService->setDefaultValue(SystemSettingKey::SYSTEM_NAME);
// Act
$result = (string) $appName;
// Assert
expect($result)->toBe(SystemConfig::DEFAULT_SYSTEM_NAME);
});
test('LowStockMultiplier returns multiplier from configuration', function (): void {
// Arrange
$lowStockMultiplier = $this->getContainer()->get(LowStockMultiplier::class);
$configService = $this->getContainer()->get(ConfigurationService::class);
$testMultiplier = '0.5';
// Set a custom multiplier
$configService->setConfigValue(SystemSettingKey::STOCK_LOW_MULTIPLIER, $testMultiplier);
// Act
$result = $lowStockMultiplier->getValue();
// Assert
expect($result)->toBe((float) $testMultiplier);
});
test('LowStockMultiplier returns default multiplier when not configured', function (): void {
// Arrange
$lowStockMultiplier = $this->getContainer()->get(LowStockMultiplier::class);
$configService = $this->getContainer()->get(ConfigurationService::class);
// Reset to default value
$configService->setDefaultValue(SystemSettingKey::STOCK_LOW_MULTIPLIER);
// Act
$result = $lowStockMultiplier->getValue();
// Assert
expect($result)->toBe((float) SystemConfig::DEFAULT_STOCK_LOW_MULTIPLIER);
});
test('LowStockMultiplier converts string value to float', function (): void {
// Arrange
$lowStockMultiplier = $this->getContainer()->get(LowStockMultiplier::class);
$configService = $this->getContainer()->get(ConfigurationService::class);
$testMultiplier = '0.75';
// Set a custom multiplier
$configService->setConfigValue(SystemSettingKey::STOCK_LOW_MULTIPLIER, $testMultiplier);
// Act
$result = $lowStockMultiplier->getValue();
// Assert
expect($result)->toBe(0.75);
expect($result)->toBeFloat();
});

View file

@ -6,142 +6,52 @@ use App\Entity\SystemConfig;
use App\Enum\SystemSettingKey; use App\Enum\SystemSettingKey;
use App\Service\ConfigurationService; use App\Service\ConfigurationService;
test('getAllConfigs returns all configurations', function (): void {
// Arrange test('get returns correct value', function (): void {
$configService = $this->getContainer()->get(ConfigurationService::class);
// Act
$configs = $configService->getAllConfigs();
// Assert
expect($configs)->toBeArray();
});
test('getConfigValue returns correct value', function (): void {
// Arrange // Arrange
/** @var ConfigurationService $configService */
$configService = $this->getContainer()->get(ConfigurationService::class); $configService = $this->getContainer()->get(ConfigurationService::class);
$key = SystemSettingKey::SYSTEM_NAME; $key = SystemSettingKey::SYSTEM_NAME;
$expectedValue = SystemSettingKey::getDefaultValue($key);
// Act // Act
$value = $configService->getConfigValue($key); $value = $configService->get($key);
// Assert // Assert
expect($value)->toBe($expectedValue); expect($value)->toBe($key->defaultValue());
}); });
test('setConfigValue updates configuration value', function (): void { test('setConfigValue updates configuration value', function (): void {
// Arrange // Arrange
/** @var ConfigurationService $configService */
$configService = $this->getContainer()->get(ConfigurationService::class); $configService = $this->getContainer()->get(ConfigurationService::class);
$key = SystemSettingKey::SYSTEM_NAME; $key = SystemSettingKey::SYSTEM_NAME;
$newValue = 'Test System Name'; $newValue = 'Test System Name';
// Act // Act
$configService->setConfigValue($key, $newValue); $configService->set($key, $newValue);
$value = $configService->getConfigValue($key); $value = $configService->get($key);
// Assert // Assert
expect($value)->toBe($newValue); expect($value)->toBe($newValue);
}); });
test('getConfigByKey returns correct config', function (): void {
// Arrange
$configService = $this->getContainer()->get(ConfigurationService::class);
$key = SystemSettingKey::SYSTEM_NAME;
// Act
$config = $configService->getConfigByKey($key);
// Assert
expect($config)->toBeInstanceOf(SystemConfig::class)
->and($config->getKey())->toBe($key);
});
test('createConfig throws exception when config already exists', function (): void {
// Arrange
$configService = $this->getContainer()->get(ConfigurationService::class);
$key = SystemSettingKey::SYSTEM_NAME;
$value = 'Test System Name';
// Ensure config exists
$configService->setConfigValue($key, $value);
// Act & Assert
expect(fn() => $configService->createConfig($key, $value))
->toThrow(InvalidArgumentException::class);
});
test('updateConfig updates configuration value', function (): void {
// Arrange
$configService = $this->getContainer()->get(ConfigurationService::class);
$key = SystemSettingKey::SYSTEM_NAME;
$initialValue = 'Initial System Name';
$newValue = 'Updated System Name';
// Create or update config with initial value
$configService->setConfigValue($key, $initialValue);
$config = $configService->getConfigByKey($key);
// Act
$updatedConfig = $configService->updateConfig($config, $newValue);
// Assert
expect($updatedConfig->getValue())->toBe($newValue)
->and($configService->getConfigValue($key))->toBe($newValue);
});
test('updateConfig does not update when value is empty', function (): void {
// Arrange
$configService = $this->getContainer()->get(ConfigurationService::class);
$key = SystemSettingKey::SYSTEM_NAME;
$initialValue = 'Initial System Name';
// Create or update config with initial value
$configService->setConfigValue($key, $initialValue);
$config = $configService->getConfigByKey($key);
// Act
$updatedConfig = $configService->updateConfig($config, '');
// Assert
expect($updatedConfig->getValue())->toBe($initialValue);
expect($configService->getConfigValue($key))->toBe($initialValue);
});
test('resetAllConfigs resets all configurations to default values', function (): void { test('resetAllConfigs resets all configurations to default values', function (): void {
// Arrange // Arrange
/** @var ConfigurationService $configService */
$configService = $this->getContainer()->get(ConfigurationService::class); $configService = $this->getContainer()->get(ConfigurationService::class);
// Set non-default values for all configs // Set non-default values for all configs
foreach (SystemSettingKey::cases() as $key) { foreach (SystemSettingKey::cases() as $key) {
$configService->setConfigValue($key, 'non-default-value'); $configService->set($key, 'non-default value');
} }
// Act // Act
$configService->resetAllConfigs(); $configService->resetAll();
// Assert // Assert
foreach (SystemSettingKey::cases() as $key) { foreach (SystemSettingKey::cases() as $key) {
$expectedValue = SystemSettingKey::getDefaultValue($key);
$actualValue = $configService->getConfigValue($key); expect($configService->get($key))->toBe($key->defaultValue());
expect($actualValue)->toBe($expectedValue);
} }
}); });
test('setDefaultValue sets default value for specific key', function (): void {
// Arrange
$configService = $this->getContainer()->get(ConfigurationService::class);
$key = SystemSettingKey::SYSTEM_NAME;
$nonDefaultValue = 'Non-Default System Name';
// Set non-default value
$configService->setConfigValue($key, $nonDefaultValue);
// Act
$configService->setDefaultValue($key);
// Assert
$expectedValue = SystemSettingKey::getDefaultValue($key);
$actualValue = $configService->getConfigValue($key);
expect($actualValue)->toBe($expectedValue);
});

View file

@ -1,243 +0,0 @@
<?php
declare(strict_types=1);
use App\Entity\DrinkType;
use App\Enum\SystemSettingKey;
use App\Service\ConfigurationService;
use App\Service\DrinkTypeService;
use Doctrine\ORM\EntityManagerInterface;
test('getAllDrinkTypes returns all drink types', function (): void {
// Arrange
$drinkTypeService = $this->getContainer()->get(DrinkTypeService::class);
// Act
$drinkTypes = $drinkTypeService->getAllDrinkTypes();
// Assert
expect($drinkTypes)->toBeArray();
});
test('getDrinkTypeById returns correct drink type', function (): void {
// Arrange
$drinkTypeService = $this->getContainer()->get(DrinkTypeService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create a drink type
$drinkType = new DrinkType();
$drinkType->setName('Test Drink Type');
$drinkType->setDescription('Test Description');
$drinkType->setDesiredStock(5);
$em->persist($drinkType);
$em->flush();
$id = $drinkType->getId();
// Act
$retrievedDrinkType = $drinkTypeService->getDrinkTypeById($id);
// Assert
expect($retrievedDrinkType)->toBeInstanceOf(DrinkType::class);
expect($retrievedDrinkType->getId())->toBe($id);
expect($retrievedDrinkType->getName())->toBe('Test Drink Type');
});
test('getDrinkTypeById returns null for non-existent id', function (): void {
// Arrange
$drinkTypeService = $this->getContainer()->get(DrinkTypeService::class);
$nonExistentId = 9999;
// Act
$drinkType = $drinkTypeService->getDrinkTypeById($nonExistentId);
// Assert
expect($drinkType)->toBeNull();
});
test('getDrinkTypeByName returns correct drink type', function (): void {
// Arrange
$drinkTypeService = $this->getContainer()->get(DrinkTypeService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create a drink type
$drinkType = new DrinkType();
$drinkType->setName('Test Drink Type By Name');
$drinkType->setDescription('Test Description');
$drinkType->setDesiredStock(5);
$em->persist($drinkType);
$em->flush();
// Act
$retrievedDrinkType = $drinkTypeService->getDrinkTypeByName('Test Drink Type By Name');
// Assert
expect($retrievedDrinkType)->toBeInstanceOf(DrinkType::class);
expect($retrievedDrinkType->getName())->toBe('Test Drink Type By Name');
});
test('getDrinkTypeByName returns null for non-existent name', function (): void {
// Arrange
$drinkTypeService = $this->getContainer()->get(DrinkTypeService::class);
$nonExistentName = 'Non-Existent Drink Type';
// Act
$drinkType = $drinkTypeService->getDrinkTypeByName($nonExistentName);
// Assert
expect($drinkType)->toBeNull();
});
test('createDrinkType creates new drink type with provided values', function (): void {
// Arrange
$drinkTypeService = $this->getContainer()->get(DrinkTypeService::class);
$name = 'New Drink Type';
$description = 'New Description';
$desiredStock = 10;
// Act
$drinkType = $drinkTypeService->createDrinkType($name, $description, $desiredStock);
// Assert
expect($drinkType)->toBeInstanceOf(DrinkType::class);
expect($drinkType->getName())->toBe($name);
expect($drinkType->getDescription())->toBe($description);
expect($drinkType->getDesiredStock())->toBe($desiredStock);
});
test('createDrinkType creates new drink type with default desired stock', function (): void {
// Arrange
$drinkTypeService = $this->getContainer()->get(DrinkTypeService::class);
$configService = $this->getContainer()->get(ConfigurationService::class);
$name = 'New Drink Type Default Stock';
$description = 'New Description';
// Set default desired stock in configuration
$defaultDesiredStock = '15';
$configService->setConfigValue(SystemSettingKey::DEFAULT_DESIRED_STOCK, $defaultDesiredStock);
// Act
$drinkType = $drinkTypeService->createDrinkType($name, $description);
// Assert
expect($drinkType)->toBeInstanceOf(DrinkType::class);
expect($drinkType->getName())->toBe($name);
expect($drinkType->getDescription())->toBe($description);
expect($drinkType->getDesiredStock())->toBe((int) $defaultDesiredStock);
});
test('createDrinkType throws exception when drink type with same name exists', function (): void {
// Arrange
$drinkTypeService = $this->getContainer()->get(DrinkTypeService::class);
$name = 'Duplicate Drink Type';
// Create a drink type with the same name
$drinkTypeService->createDrinkType($name);
// Act & Assert
expect(fn() => $drinkTypeService->createDrinkType($name))
->toThrow(InvalidArgumentException::class);
});
test('updateDrinkType updates drink type properties', function (): void {
// Arrange
$drinkTypeService = $this->getContainer()->get(DrinkTypeService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create a drink type
$drinkType = new DrinkType();
$drinkType->setName('Original Drink Type');
$drinkType->setDescription('Original Description');
$drinkType->setDesiredStock(5);
$em->persist($drinkType);
$em->flush();
$newName = 'Updated Drink Type';
$newDescription = 'Updated Description';
$newDesiredStock = 15;
// Act
$updatedDrinkType = $drinkTypeService->updateDrinkType(
$drinkType,
$newName,
$newDescription,
$newDesiredStock
);
// Assert
expect($updatedDrinkType)->toBeInstanceOf(DrinkType::class);
expect($updatedDrinkType->getName())->toBe($newName);
expect($updatedDrinkType->getDescription())->toBe($newDescription);
expect($updatedDrinkType->getDesiredStock())->toBe($newDesiredStock);
});
test('updateDrinkType throws exception when updating to existing name', function (): void {
// Arrange
$drinkTypeService = $this->getContainer()->get(DrinkTypeService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create two drink types
$drinkType1 = new DrinkType();
$drinkType1->setName('First Drink Type');
$em->persist($drinkType1);
$drinkType2 = new DrinkType();
$drinkType2->setName('Second Drink Type');
$em->persist($drinkType2);
$em->flush();
// Act & Assert
expect(fn() => $drinkTypeService->updateDrinkType($drinkType2, 'First Drink Type'))
->toThrow(InvalidArgumentException::class);
});
test('updateDrinkType only updates provided properties', function (): void {
// Arrange
$drinkTypeService = $this->getContainer()->get(DrinkTypeService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create a drink type
$drinkType = new DrinkType();
$drinkType->setName('Partial Update Drink Type');
$drinkType->setDescription('Original Description');
$drinkType->setDesiredStock(5);
$em->persist($drinkType);
$em->flush();
$newDescription = 'Updated Description';
// Act - only update description
$updatedDrinkType = $drinkTypeService->updateDrinkType(
$drinkType,
null,
$newDescription,
null
);
// Assert
expect($updatedDrinkType)->toBeInstanceOf(DrinkType::class);
expect($updatedDrinkType->getName())->toBe('Partial Update Drink Type');
expect($updatedDrinkType->getDescription())->toBe($newDescription);
expect($updatedDrinkType->getDesiredStock())->toBe(5);
});
test('deleteDrinkType removes drink type', function (): void {
// Arrange
$drinkTypeService = $this->getContainer()->get(DrinkTypeService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create a drink type
$drinkType = new DrinkType();
$drinkType->setName('Drink Type To Delete');
$em->persist($drinkType);
$em->flush();
$id = $drinkType->getId();
// Act
$drinkTypeService->deleteDrinkType($drinkType);
// Assert
$deletedDrinkType = $drinkTypeService->getDrinkTypeById($id);
expect($deletedDrinkType)->toBeNull();
});

View file

@ -1,366 +0,0 @@
<?php
declare(strict_types=1);
use App\Entity\DrinkType;
use App\Entity\InventoryRecord;
use App\Enum\StockState;
use App\Enum\SystemSettingKey;
use App\Repository\DrinkTypeRepository;
use App\Repository\InventoryRecordRepository;
use App\Service\ConfigurationService;
use App\Service\InventoryService;
use App\ValueObject\DrinkStock;
use Doctrine\ORM\EntityManagerInterface;
test('getAllInventoryRecords returns all inventory records', function (): void {
// Arrange
$inventoryService = $this->getContainer()->get(InventoryService::class);
// Act
$records = $inventoryService->getAllInventoryRecords();
// Assert
expect($records)->toBeArray();
});
test('getInventoryRecordsByDrinkType returns records for specific drink type', function (): void {
// Arrange
$inventoryService = $this->getContainer()->get(InventoryService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create a drink type
$drinkType = new DrinkType();
$drinkType->setName('Test Drink Type for Inventory');
$drinkType->setDesiredStock(10);
$em->persist($drinkType);
$em->flush();
// Create inventory records
$record1 = new InventoryRecord();
$record1->setDrinkType($drinkType);
$record1->setQuantity(5);
$em->persist($record1);
$record2 = new InventoryRecord();
$record2->setDrinkType($drinkType);
$record2->setQuantity(8);
$em->persist($record2);
$em->flush();
// Act
$records = $inventoryService->getInventoryRecordsByDrinkType($drinkType);
// Assert
expect($records)->toBeArray();
expect(count($records))->toBeGreaterThanOrEqual(2);
expect($records[0])->toBeInstanceOf(InventoryRecord::class);
expect($records[0]->getDrinkType()->getId())->toBe($drinkType->getId());
});
test('getLatestInventoryRecord returns latest record for drink type', function (): void {
// Arrange
$inventoryService = $this->getContainer()->get(InventoryService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create a drink type
$drinkType = new DrinkType();
$drinkType->setName('Test Drink Type for Latest Record');
$drinkType->setDesiredStock(10);
$em->persist($drinkType);
$em->flush();
// Create inventory records with different timestamps
$record1 = new InventoryRecord();
$record1->setDrinkType($drinkType);
$record1->setQuantity(5);
$record1->setTimestamp(new DateTimeImmutable('-2 days'));
$em->persist($record1);
$record2 = new InventoryRecord();
$record2->setDrinkType($drinkType);
$record2->setQuantity(8);
$record2->setTimestamp(new DateTimeImmutable('-1 day'));
$em->persist($record2);
$record3 = new InventoryRecord();
$record3->setDrinkType($drinkType);
$record3->setQuantity(12);
$record3->setTimestamp(new DateTimeImmutable());
$em->persist($record3);
$em->flush();
// Act
$latestRecord = $inventoryService->getLatestInventoryRecord($drinkType);
// Assert
expect($latestRecord)->toBeInstanceOf(InventoryRecord::class);
expect($latestRecord->getDrinkType()->getId())->toBe($drinkType->getId());
expect($latestRecord->getQuantity())->toBe(12);
});
test('getLatestInventoryRecord creates new record if none exists', function (): void {
// Arrange
$inventoryService = $this->getContainer()->get(InventoryService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
$repository = $this->getContainer()->get(InventoryRecordRepository::class);
// Create a drink type with no inventory records
$drinkType = new DrinkType();
$drinkType->setName('Test Drink Type with No Records');
$drinkType->setDesiredStock(10);
$em->persist($drinkType);
$em->flush();
// Delete any existing records for this drink type
foreach ($repository->findByDrinkType($drinkType) as $record) {
$em->remove($record);
}
$em->flush();
// Act
$latestRecord = $inventoryService->getLatestInventoryRecord($drinkType);
// Assert
expect($latestRecord)->toBeInstanceOf(InventoryRecord::class);
expect($latestRecord->getDrinkType()->getId())->toBe($drinkType->getId());
expect($latestRecord->getQuantity())->toBe(0);
});
test('getCurrentStockLevel returns correct stock level', function (): void {
// Arrange
$inventoryService = $this->getContainer()->get(InventoryService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create a drink type
$drinkType = new DrinkType();
$drinkType->setName('Test Drink Type for Stock Level');
$drinkType->setDesiredStock(10);
$em->persist($drinkType);
$em->flush();
// Create inventory record
$record = new InventoryRecord();
$record->setDrinkType($drinkType);
$record->setQuantity(15);
$em->persist($record);
$em->flush();
// Act
$stockLevel = $inventoryService->getCurrentStockLevel($drinkType);
// Assert
expect($stockLevel)->toBe(15);
});
test('updateStockLevel creates new inventory record', function (): void {
// Arrange
$inventoryService = $this->getContainer()->get(InventoryService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create a drink type
$drinkType = new DrinkType();
$drinkType->setName('Test Drink Type for Update');
$drinkType->setDesiredStock(10);
$em->persist($drinkType);
$em->flush();
$newQuantity = 25;
$timestamp = new DateTimeImmutable();
// Act
$record = $inventoryService->updateStockLevel($drinkType, $newQuantity, $timestamp);
// Assert
expect($record)->toBeInstanceOf(InventoryRecord::class);
expect($record->getDrinkType()->getId())->toBe($drinkType->getId());
expect($record->getQuantity())->toBe($newQuantity);
expect($record->getTimestamp()->getTimestamp())->toBe($timestamp->getTimestamp());
// Verify the stock level was updated
$currentLevel = $inventoryService->getCurrentStockLevel($drinkType);
expect($currentLevel)->toBe($newQuantity);
});
test('getAllDrinkTypesWithStockLevels returns all drink types with stock', function (): void {
// Arrange
$inventoryService = $this->getContainer()->get(InventoryService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
$drinkTypeRepo = $this->getContainer()->get(DrinkTypeRepository::class);
// Create drink types and inventory records
$drinkType1 = new DrinkType();
$drinkType1->setName('Drink Type 1 for Stock Levels');
$drinkType1->setDesiredStock(10);
$em->persist($drinkType1);
$drinkType2 = new DrinkType();
$drinkType2->setName('Drink Type 2 for Stock Levels');
$drinkType2->setDesiredStock(0); // Zero desired stock
$em->persist($drinkType2);
$em->flush();
// Create inventory records
$record1 = new InventoryRecord();
$record1->setDrinkType($drinkType1);
$record1->setQuantity(5);
$em->persist($record1);
$record2 = new InventoryRecord();
$record2->setDrinkType($drinkType2);
$record2->setQuantity(8);
$em->persist($record2);
$em->flush();
// Act - without zero desired stock
$stockLevels1 = $inventoryService->getAllDrinkTypesWithStockLevels(false);
// Act - with zero desired stock
$stockLevels2 = $inventoryService->getAllDrinkTypesWithStockLevels(true);
// Assert
expect($stockLevels1)->toBeArray();
expect($stockLevels2)->toBeArray();
expect(count($stockLevels2))->toBeGreaterThanOrEqual(count($stockLevels1));
foreach ($stockLevels2 as $stockLevel) {
expect($stockLevel)->toBeInstanceOf(DrinkStock::class);
expect($stockLevel->record)->toBeInstanceOf(InventoryRecord::class);
}
});
test('getDrinkStock returns correct DrinkStock object with CRITICAL state', function (): void {
// Arrange
$inventoryService = $this->getContainer()->get(InventoryService::class);
$configService = $this->getContainer()->get(ConfigurationService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Set low stock multiplier
$configService->setConfigValue(SystemSettingKey::STOCK_LOW_MULTIPLIER, '0.3');
// Create a drink type with zero quantity (CRITICAL)
$drinkType = new DrinkType();
$drinkType->setName('Critical Stock Drink Type');
$drinkType->setDesiredStock(10);
$em->persist($drinkType);
$em->flush();
// Create inventory record with zero quantity
$record = new InventoryRecord();
$record->setDrinkType($drinkType);
$record->setQuantity(0);
$em->persist($record);
$em->flush();
// Act
$drinkStock = $inventoryService->getDrinkStock($drinkType);
// Assert
expect($drinkStock)->toBeInstanceOf(DrinkStock::class);
expect($drinkStock->record->getDrinkType()->getId())->toBe($drinkType->getId());
expect($drinkStock->stock)->toBe(StockState::CRITICAL);
});
test('getDrinkStock returns correct DrinkStock object with LOW state', function (): void {
// Arrange
$inventoryService = $this->getContainer()->get(InventoryService::class);
$configService = $this->getContainer()->get(ConfigurationService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Set low stock multiplier
$lowStockMultiplier = 0.3;
$configService->setConfigValue(SystemSettingKey::STOCK_LOW_MULTIPLIER, (string) $lowStockMultiplier);
// Create a drink type with low quantity
$desiredStock = 10;
$drinkType = new DrinkType();
$drinkType->setName('Low Stock Drink Type');
$drinkType->setDesiredStock($desiredStock);
$em->persist($drinkType);
$em->flush();
// Create inventory record with low quantity (between 0 and lowStockMultiplier * desiredStock)
$lowQuantity = (int) ($desiredStock * $lowStockMultiplier) - 1;
$record = new InventoryRecord();
$record->setDrinkType($drinkType);
$record->setQuantity($lowQuantity);
$em->persist($record);
$em->flush();
// Act
$drinkStock = $inventoryService->getDrinkStock($drinkType);
// Assert
expect($drinkStock)->toBeInstanceOf(DrinkStock::class);
expect($drinkStock->record->getDrinkType()->getId())->toBe($drinkType->getId());
expect($drinkStock->stock)->toBe(StockState::LOW);
});
test('getDrinkStock returns correct DrinkStock object with NORMAL state', function (): void {
// Arrange
$inventoryService = $this->getContainer()->get(InventoryService::class);
$configService = $this->getContainer()->get(ConfigurationService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Set low stock multiplier
$lowStockMultiplier = 0.3;
$configService->setConfigValue(SystemSettingKey::STOCK_LOW_MULTIPLIER, (string) $lowStockMultiplier);
// Create a drink type with normal quantity
$desiredStock = 10;
$drinkType = new DrinkType();
$drinkType->setName('Normal Stock Drink Type');
$drinkType->setDesiredStock($desiredStock);
$em->persist($drinkType);
$em->flush();
// Create inventory record with normal quantity (between lowStockMultiplier * desiredStock and desiredStock)
$normalQuantity = (int) ($desiredStock * $lowStockMultiplier) + 1;
$record = new InventoryRecord();
$record->setDrinkType($drinkType);
$record->setQuantity($normalQuantity);
$em->persist($record);
$em->flush();
// Act
$drinkStock = $inventoryService->getDrinkStock($drinkType);
// Assert
expect($drinkStock)->toBeInstanceOf(DrinkStock::class);
expect($drinkStock->record->getDrinkType()->getId())->toBe($drinkType->getId());
expect($drinkStock->stock)->toBe(StockState::NORMAL);
});
test('getDrinkStock returns correct DrinkStock object with HIGH state', function (): void {
// Arrange
$inventoryService = $this->getContainer()->get(InventoryService::class);
$configService = $this->getContainer()->get(ConfigurationService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Set low stock multiplier
$configService->setConfigValue(SystemSettingKey::STOCK_LOW_MULTIPLIER, '0.3');
// Create a drink type with high quantity
$desiredStock = 10;
$drinkType = new DrinkType();
$drinkType->setName('High Stock Drink Type');
$drinkType->setDesiredStock($desiredStock);
$em->persist($drinkType);
$em->flush();
// Create inventory record with high quantity (greater than desiredStock)
$highQuantity = $desiredStock + 1;
$record = new InventoryRecord();
$record->setDrinkType($drinkType);
$record->setQuantity($highQuantity);
$em->persist($record);
$em->flush();
// Act
$drinkStock = $inventoryService->getDrinkStock($drinkType);
// Assert
expect($drinkStock)->toBeInstanceOf(DrinkStock::class);
expect($drinkStock->record->getDrinkType()->getId())->toBe($drinkType->getId());
expect($drinkStock->stock)->toBe(StockState::HIGH);
});

View file

@ -1,461 +0,0 @@
<?php
declare(strict_types=1);
use App\Entity\DrinkType;
use App\Entity\Order;
use App\Entity\OrderItem;
use App\Enum\OrderStatus;
use App\Repository\OrderItemRepository;
use App\Service\InventoryService;
use App\Service\OrderService;
use Doctrine\ORM\EntityManagerInterface;
test('getAllOrders returns all orders', function (): void {
// Arrange
$orderService = $this->getContainer()->get(OrderService::class);
// Act
$orders = $orderService->getAllOrders();
// Assert
expect($orders)->toBeArray();
});
test('getOrderById returns correct order', function (): void {
// Arrange
$orderService = $this->getContainer()->get(OrderService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create an order
$order = new Order();
$order->setStatus(OrderStatus::NEW);
$em->persist($order);
$em->flush();
$id = $order->getId();
// Act
$retrievedOrder = $orderService->getOrderById($id);
// Assert
expect($retrievedOrder)->toBeInstanceOf(Order::class);
expect($retrievedOrder->getId())->toBe($id);
});
test('getOrderById returns null for non-existent id', function (): void {
// Arrange
$orderService = $this->getContainer()->get(OrderService::class);
$nonExistentId = 9999;
// Act
$order = $orderService->getOrderById($nonExistentId);
// Assert
expect($order)->toBeNull();
});
test('getOrdersByStatus returns orders with specific status', function (): void {
// Arrange
$orderService = $this->getContainer()->get(OrderService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create orders with different statuses
$order1 = new Order();
$order1->setStatus(OrderStatus::NEW);
$em->persist($order1);
$order2 = new Order();
$order2->setStatus(OrderStatus::ORDERED);
$em->persist($order2);
$order3 = new Order();
$order3->setStatus(OrderStatus::NEW);
$em->persist($order3);
$em->flush();
// Act
$newOrders = $orderService->getOrdersByStatus(OrderStatus::NEW);
$orderedOrders = $orderService->getOrdersByStatus(OrderStatus::ORDERED);
// Assert
expect($newOrders)->toBeArray();
expect(count($newOrders))->toBeGreaterThanOrEqual(2);
expect($orderedOrders)->toBeArray();
expect(count($orderedOrders))->toBeGreaterThanOrEqual(1);
foreach ($newOrders as $order) {
expect($order)->toBeInstanceOf(Order::class);
expect($order->getStatus())->toBe(OrderStatus::NEW);
}
foreach ($orderedOrders as $order) {
expect($order)->toBeInstanceOf(Order::class);
expect($order->getStatus())->toBe(OrderStatus::ORDERED);
}
});
test('getActiveOrders returns orders with active statuses', function (): void {
// Arrange
$orderService = $this->getContainer()->get(OrderService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create orders with different statuses
$order1 = new Order();
$order1->setStatus(OrderStatus::NEW);
$em->persist($order1);
$order2 = new Order();
$order2->setStatus(OrderStatus::ORDERED);
$em->persist($order2);
$order3 = new Order();
$order3->setStatus(OrderStatus::FULFILLED);
$em->persist($order3);
$order4 = new Order();
$order4->setStatus(OrderStatus::CANCELLED);
$em->persist($order4);
$em->flush();
// Act
$activeOrders = $orderService->getActiveOrders();
// Assert
expect($activeOrders)->toBeArray();
foreach ($activeOrders as $order) {
expect($order)->toBeInstanceOf(Order::class);
expect($order->getStatus())->toBeIn([OrderStatus::NEW, OrderStatus::ORDERED, OrderStatus::IN_WORK]);
}
});
test('getMostRecentActiveOrder returns most recent active order', function (): void {
// Arrange
$orderService = $this->getContainer()->get(OrderService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create orders with different statuses and timestamps
$order1 = new Order();
$order1->setStatus(OrderStatus::NEW);
$order1->setUpdatedAt(new DateTimeImmutable('-2 days'));
$em->persist($order1);
$em->flush();
// Sleep to ensure different timestamps
$order2 = new Order();
$order2->setStatus(OrderStatus::IN_WORK);
$order2->setUpdatedAt(new DateTimeImmutable('-1 day'));
$em->persist($order2);
$em->flush();
// Act
$recentOrder = $orderService->getMostRecentActiveOrder();
// Assert
expect($recentOrder)->toBeInstanceOf(Order::class);
expect($recentOrder->getId())->toBe($order2->getId());
});
test('hasActiveOrders returns true when active orders exist', function (): void {
// Arrange
$orderService = $this->getContainer()->get(OrderService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create an active order
$order = new Order();
$order->setStatus(OrderStatus::NEW);
$em->persist($order);
$em->flush();
// Act
$hasActiveOrders = $orderService->hasActiveOrders();
// Assert
expect($hasActiveOrders)->toBeTrue();
});
test('getOrdersByDateRange returns orders within date range', function (): void {
// Arrange
$orderService = $this->getContainer()->get(OrderService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create orders
$order1 = new Order();
$order1->setStatus(OrderStatus::NEW);
$em->persist($order1);
$em->flush();
$start = new DateTimeImmutable('-1 day');
$end = new DateTimeImmutable('+1 day');
// Act
$orders = $orderService->getOrdersByDateRange($start, $end);
// Assert
expect($orders)->toBeArray();
expect(count($orders))->toBeGreaterThanOrEqual(1);
foreach ($orders as $order) {
expect($order)->toBeInstanceOf(Order::class);
expect($order->getCreatedAt()->getTimestamp())->toBeGreaterThanOrEqual($start->getTimestamp());
expect($order->getCreatedAt()->getTimestamp())->toBeLessThanOrEqual($end->getTimestamp());
}
});
test('createOrder creates new order with items', function (): void {
// Arrange
$orderService = $this->getContainer()->get(OrderService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create drink types
$drinkType1 = new DrinkType();
$drinkType1->setName('Drink Type 1 for Order');
$drinkType1->setDesiredStock(10);
$em->persist($drinkType1);
$drinkType2 = new DrinkType();
$drinkType2->setName('Drink Type 2 for Order');
$drinkType2->setDesiredStock(5);
$em->persist($drinkType2);
$em->flush();
$items = [
[
'drinkTypeId' => $drinkType1->getId(),
'quantity' => 3,
],
[
'drinkTypeId' => $drinkType2->getId(),
'quantity' => 2,
],
];
// Act
$order = $orderService->createOrder($items);
// Assert
expect($order)->toBeInstanceOf(Order::class);
expect($order->getStatus())->toBe(OrderStatus::NEW);
expect($order->getOrderItems()->count())->toBe(2);
$orderItems = $order->getOrderItems()->toArray();
expect($orderItems[0]->getDrinkType()->getId())->toBe($drinkType1->getId());
expect($orderItems[0]->getQuantity())->toBe(3);
expect($orderItems[1]->getDrinkType()->getId())->toBe($drinkType2->getId());
expect($orderItems[1]->getQuantity())->toBe(2);
});
test('createOrderFromStockLevels creates order based on stock levels', function (): void {
// Arrange
$orderService = $this->getContainer()->get(OrderService::class);
$inventoryService = $this->getContainer()->get(InventoryService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create drink types with stock levels below desired stock
$drinkType1 = new DrinkType();
$drinkType1->setName('Low Stock Drink Type 1');
$drinkType1->setDesiredStock(10);
$em->persist($drinkType1);
$drinkType2 = new DrinkType();
$drinkType2->setName('Low Stock Drink Type 2');
$drinkType2->setDesiredStock(8);
$em->persist($drinkType2);
$drinkType3 = new DrinkType();
$drinkType3->setName('Sufficient Stock Drink Type');
$drinkType3->setDesiredStock(5);
$em->persist($drinkType3);
$em->flush();
// Set stock levels
$inventoryService->updateStockLevel($drinkType1, 2); // Low stock
$inventoryService->updateStockLevel($drinkType2, 3); // Low stock
$inventoryService->updateStockLevel($drinkType3, 10); // Sufficient stock
// Act
$order = $orderService->createOrderFromStockLevels();
// Assert
expect($order)->toBeInstanceOf(Order::class);
expect($order->getStatus())->toBe(OrderStatus::NEW);
// Should only include items for drink types with low stock
$orderItems = $order->getOrderItems()->toArray();
$drinkTypeIds = array_map(fn($item) => $item->getDrinkType()->getId(), $orderItems);
expect($drinkTypeIds)->toContain($drinkType1->getId());
expect($drinkTypeIds)->toContain($drinkType2->getId());
expect($drinkTypeIds)->not->toContain($drinkType3->getId());
// Check quantities
foreach ($orderItems as $item) {
$drinkType = $item->getDrinkType();
$currentStock = $inventoryService->getCurrentStockLevel($drinkType);
$desiredStock = $drinkType->getDesiredStock();
$expectedQuantity = $desiredStock - $currentStock;
expect($item->getQuantity())->toBe($expectedQuantity);
}
});
test('updateOrderStatus updates order status', function (): void {
// Arrange
$orderService = $this->getContainer()->get(OrderService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create an order
$order = new Order();
$order->setStatus(OrderStatus::NEW);
$em->persist($order);
$em->flush();
// Act
$updatedOrder = $orderService->updateOrderStatus($order, OrderStatus::ORDERED);
// Assert
expect($updatedOrder)->toBeInstanceOf(Order::class);
expect($updatedOrder->getStatus())->toBe(OrderStatus::ORDERED);
// Verify the status was updated in the database
});
test('addOrderItem adds item to order', function (): void {
// Arrange
$orderService = $this->getContainer()->get(OrderService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create an order
$order = new Order();
$order->setStatus(OrderStatus::NEW);
$em->persist($order);
// Create a drink type
$drinkType = new DrinkType();
$drinkType->setName('Drink Type for Order Item');
$drinkType->setDesiredStock(10);
$em->persist($drinkType);
$em->flush();
$quantity = 5;
// Act
$orderItem = $orderService->addOrderItem($order, $drinkType, $quantity);
// Assert
expect($orderItem)->toBeInstanceOf(OrderItem::class);
expect($orderItem->getOrder()->getId())->toBe($order->getId());
expect($orderItem->getDrinkType()->getId())->toBe($drinkType->getId());
expect($orderItem->getQuantity())->toBe($quantity);
// Verify the item was added to the order
expect($order->getOrderItems()->contains($orderItem))->toBeTrue();
});
test('addOrderItem updates quantity if item already exists', function (): void {
// Arrange
$orderService = $this->getContainer()->get(OrderService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create an order
$order = new Order();
$order->setStatus(OrderStatus::NEW);
$em->persist($order);
// Create a drink type
$drinkType = new DrinkType();
$drinkType->setName('Drink Type for Existing Order Item');
$drinkType->setDesiredStock(10);
$em->persist($drinkType);
$em->flush();
// Add an item
$initialQuantity = 3;
$orderItem = $orderService->addOrderItem($order, $drinkType, $initialQuantity);
// Act - add another item with the same drink type
$additionalQuantity = 2;
$updatedOrderItem = $orderService->addOrderItem($order, $drinkType, $additionalQuantity);
// Assert
expect($updatedOrderItem)->toBeInstanceOf(OrderItem::class);
expect($updatedOrderItem->getId())->toBe($orderItem->getId());
expect($updatedOrderItem->getQuantity())->toBe($initialQuantity + $additionalQuantity);
// Verify the order still has only one item for this drink type
$matchingItems = $order->getOrderItems()->filter(
fn($item): bool => $item->getDrinkType()->getId() === $drinkType->getId()
);
expect($matchingItems->count())->toBe(1);
});
test('removeOrderItem removes item from order', function (): void {
// Arrange
$orderService = $this->getContainer()->get(OrderService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create an order
$order = new Order();
$order->setStatus(OrderStatus::NEW);
$em->persist($order);
// Create a drink type
$drinkType = new DrinkType();
$drinkType->setName('Drink Type for Order Item Removal');
$drinkType->setDesiredStock(10);
$em->persist($drinkType);
$em->flush();
// Add an item
$orderItem = $orderService->addOrderItem($order, $drinkType, 5);
// Act
$orderService->removeOrderItem($order, $orderItem);
// Assert
expect($order->getOrderItems()->contains($orderItem))->toBeFalse();
// Verify the item was removed from the database
$em->refresh($order);
$matchingItems = $order->getOrderItems()->filter(
fn($item): bool => $item->getDrinkType()->getId() === $drinkType->getId()
);
expect($matchingItems->count())->toBe(0);
});
test('deleteOrder removes order and its items', function (): void {
// Arrange
$orderService = $this->getContainer()->get(OrderService::class);
$em = $this->getContainer()->get(EntityManagerInterface::class);
// Create an order
$order = new Order();
$order->setStatus(OrderStatus::NEW);
$em->persist($order);
// Create a drink type
$drinkType = new DrinkType();
$drinkType->setName('Drink Type for Order Deletion');
$drinkType->setDesiredStock(10);
$em->persist($drinkType);
$em->flush();
// Add an item
$orderItem = $orderService->addOrderItem($order, $drinkType, 5);
$orderItemId = $orderItem->getId();
$orderId = $order->getId();
// Act
$orderService->deleteOrder($order);
// Assert
$deletedOrder = $orderService->getOrderById($orderId);
expect($deletedOrder)->toBeNull();
// Verify the order items were also deleted
$orderItemRepo = $this->getContainer()->get(OrderItemRepository::class);
$deletedOrderItem = $orderItemRepo->find($orderItemId);
expect($deletedOrderItem)->toBeNull();
});