diff --git a/composer.json b/composer.json index 0b821b83..c8ce58bb 100644 --- a/composer.json +++ b/composer.json @@ -61,6 +61,7 @@ "hiqdev/hidev-hiqdev": "dev-master", "hiqdev/php-data-mapper": "dev-master", "vimeo/psalm": "^5.0", + "opis/closure": "3.x-dev as 3.6.x-dev", "cache/array-adapter": "*", "matthiasnoback/behat-expect-exception": "^v0.3.0" }, diff --git a/src/Exception/LogicException.php b/src/Exception/LogicException.php new file mode 100644 index 00000000..695c26f9 --- /dev/null +++ b/src/Exception/LogicException.php @@ -0,0 +1,8 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\Exception; + +class LogicException extends \LogicException +{ + +} diff --git a/src/product/AggregateInterface.php b/src/product/AggregateInterface.php new file mode 100644 index 00000000..e18b4f32 --- /dev/null +++ b/src/product/AggregateInterface.php @@ -0,0 +1,8 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product; + +interface AggregateInterface +{ + public function isMax(): bool; +} diff --git a/src/product/AggregateNotDefinedException.php b/src/product/AggregateNotDefinedException.php new file mode 100644 index 00000000..e5e7887a --- /dev/null +++ b/src/product/AggregateNotDefinedException.php @@ -0,0 +1,10 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product; + +use hiqdev\php\billing\Exception\LogicException; + +class AggregateNotDefinedException extends LogicException +{ + +} diff --git a/src/product/AggregateNotFoundException.php b/src/product/AggregateNotFoundException.php new file mode 100644 index 00000000..2d9c9722 --- /dev/null +++ b/src/product/AggregateNotFoundException.php @@ -0,0 +1,9 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product; + +use hiqdev\php\billing\Exception\RuntimeException; + +class AggregateNotFoundException extends RuntimeException +{ +} diff --git a/src/product/BillingRegistry.php b/src/product/BillingRegistry.php new file mode 100644 index 00000000..5cdeb74d --- /dev/null +++ b/src/product/BillingRegistry.php @@ -0,0 +1,176 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product; + +use hiqdev\php\billing\product\behavior\InvalidBehaviorException; +use hiqdev\php\billing\product\invoice\InvalidRepresentationException; +use hiqdev\php\billing\product\invoice\RepresentationInterface; +use hiqdev\php\billing\product\price\PriceTypeDefinition; +use hiqdev\php\billing\product\quantity\QuantityFormatterInterface; +use hiqdev\php\billing\product\quantity\QuantityFormatterNotFoundException; +use hiqdev\php\billing\product\quantity\FractionQuantityData; +use hiqdev\php\billing\product\behavior\BehaviorInterface; +use hiqdev\php\billing\product\behavior\BehaviorNotFoundException; +use hiqdev\php\billing\type\Type; +use hiqdev\php\billing\type\TypeInterface; + +class BillingRegistry implements BillingRegistryInterface +{ + /** @var TariffTypeDefinition[] */ + private array $tariffTypes = []; + private bool $locked = false; + + public function addTariffType(TariffTypeDefinition $tariffType): void + { + if ($this->locked) { + throw new \RuntimeException("BillingRegistry is locked and cannot be modified."); + } + + $this->tariffTypes[] = $tariffType; + } + + public function lock(): void + { + $this->locked = true; + } + + public function priceTypes(): \Generator + { + foreach ($this->tariffTypes as $tariffType) { + foreach ($tariffType->withPrices() as $priceTypeDefinition) { + yield $priceTypeDefinition; + } + } + } + + /** + * @param string $representationClass + * @return RepresentationInterface[] + */ + public function getRepresentationsByType(string $representationClass): array + { + if (!class_exists($representationClass)) { + throw new InvalidRepresentationException("Class '$representationClass' does not exist"); + } + + if (!is_subclass_of($representationClass, RepresentationInterface::class)) { + throw new InvalidBehaviorException( + sprintf('Representation class "%s" does not implement RepresentationInterface', $representationClass) + ); + } + + $representations = []; + foreach ($this->priceTypes() as $priceTypeDefinition) { + foreach ($priceTypeDefinition->documentRepresentation() as $representation) { + if ($representation instanceof $representationClass) { + $representations[] = $representation; + } + } + } + + return $representations; + } + + public function createQuantityFormatter( + string $type, + FractionQuantityData $data, + ): QuantityFormatterInterface { + $type = $this->convertStringTypeToType($type); + + foreach ($this->priceTypes() as $priceTypeDefinition) { + if ($priceTypeDefinition->hasType($type)) { + return $priceTypeDefinition->createQuantityFormatter($data); + } + } + + throw new QuantityFormatterNotFoundException('Quantity formatter not found'); + } + + private function convertStringTypeToType(string $type): TypeInterface + { + return Type::anyId($type); + } + + /** + * @param string $type - full type like 'overuse,lb_capacity_unit' + * @param string $behaviorClassWrapper + * @return BehaviorInterface + * @throws BehaviorNotFoundException + * @throws InvalidBehaviorException + */ + public function getBehavior(string $type, string $behaviorClassWrapper): BehaviorInterface + { + if (!class_exists($behaviorClassWrapper)) { + throw new InvalidBehaviorException( + sprintf('Behavior class "%s" does not exist', $behaviorClassWrapper) + ); + } + + if (!is_subclass_of($behaviorClassWrapper, BehaviorInterface::class)) { + throw new InvalidBehaviorException( + sprintf('Behavior class "%s" does not implement BehaviorInterface', $behaviorClassWrapper) + ); + } + + $billingType = $this->convertStringTypeToType($type); + + foreach ($this->priceTypes() as $priceTypeDefinition) { + if ($priceTypeDefinition->hasType($billingType)) { + $behavior = $this->findBehaviorInPriceType($priceTypeDefinition, $behaviorClassWrapper); + + if ($behavior) { + return $behavior; + } + } + } + + throw new BehaviorNotFoundException( + sprintf('Behavior of class "%s" not found for type "%s"', $behaviorClassWrapper, $type), + ); + } + + private function findBehaviorInPriceType( + PriceTypeDefinition $priceTypeDefinition, + string $behaviorClassWrapper + ): ?BehaviorInterface { + foreach ($priceTypeDefinition->withBehaviors() as $behavior) { + if ($behavior instanceof $behaviorClassWrapper) { + return $behavior; + } + } + + return null; + } + + public function getBehaviors(string $behaviorClassWrapper): \Generator + { + foreach ($this->tariffTypes as $tariffType) { + foreach ($tariffType->withBehaviors() as $behavior) { + if ($behavior instanceof $behaviorClassWrapper) { + yield $behavior; + } + } + } + + foreach ($this->priceTypes() as $priceTypeDefinition) { + foreach ($priceTypeDefinition->withBehaviors() as $behavior) { + if ($behavior instanceof $behaviorClassWrapper) { + yield $behavior; + } + } + } + } + + public function getAggregate(string $type): AggregateInterface + { + $type = $this->convertStringTypeToType($type); + + foreach ($this->priceTypes() as $priceTypeDefinition) { + if ($priceTypeDefinition->hasType($type)) { + return $priceTypeDefinition->getAggregate(); + } + } + + throw new AggregateNotFoundException('Aggregate was not found'); + } +} diff --git a/src/product/BillingRegistryInterface.php b/src/product/BillingRegistryInterface.php new file mode 100644 index 00000000..8d52e01b --- /dev/null +++ b/src/product/BillingRegistryInterface.php @@ -0,0 +1,14 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product; + +use Generator; +use hiqdev\php\billing\product\price\PriceTypeDefinition; + +interface BillingRegistryInterface +{ + /** + * @return Generator<PriceTypeDefinition> + */ + public function priceTypes(): Generator; +} \ No newline at end of file diff --git a/src/product/DocumentRepresentationInterface.php b/src/product/DocumentRepresentationInterface.php new file mode 100644 index 00000000..83b34fe2 --- /dev/null +++ b/src/product/DocumentRepresentationInterface.php @@ -0,0 +1,8 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product; + +interface DocumentRepresentationInterface +{ + +} \ No newline at end of file diff --git a/src/product/Domain/Model/TariffTypeInterface.php b/src/product/Domain/Model/TariffTypeInterface.php new file mode 100644 index 00000000..5c8f0adc --- /dev/null +++ b/src/product/Domain/Model/TariffTypeInterface.php @@ -0,0 +1,8 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\Domain\Model; + +interface TariffTypeInterface +{ + +} diff --git a/src/product/Domain/Model/Unit/FractionUnitInterface.php b/src/product/Domain/Model/Unit/FractionUnitInterface.php new file mode 100644 index 00000000..177eeb8e --- /dev/null +++ b/src/product/Domain/Model/Unit/FractionUnitInterface.php @@ -0,0 +1,8 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\Domain\Model\Unit; + +interface FractionUnitInterface +{ + +} diff --git a/src/product/Domain/Model/Unit/UnitInterface.php b/src/product/Domain/Model/Unit/UnitInterface.php new file mode 100644 index 00000000..116d048b --- /dev/null +++ b/src/product/Domain/Model/Unit/UnitInterface.php @@ -0,0 +1,12 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\Domain\Model\Unit; + +use \hiqdev\php\units\UnitInterface as BaseUnitInterface; + +interface UnitInterface +{ + public function createExternalUnit(): BaseUnitInterface; + + public function fractionUnit(): FractionUnitInterface; +} diff --git a/src/product/GTypeInterface.php b/src/product/GTypeInterface.php new file mode 100644 index 00000000..ce3d9445 --- /dev/null +++ b/src/product/GTypeInterface.php @@ -0,0 +1,10 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product; + +interface GTypeInterface +{ + public function name(): string; + + public function equals(GTypeInterface $otherGType): bool; +} diff --git a/src/product/InvoiceDescriptionsBuilder.php b/src/product/InvoiceDescriptionsBuilder.php new file mode 100644 index 00000000..8607ba0a --- /dev/null +++ b/src/product/InvoiceDescriptionsBuilder.php @@ -0,0 +1,23 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product; + +class InvoiceDescriptionsBuilder +{ + private BillingRegistry $registry; + + public function __construct(BillingRegistry $registry) + { + $this->registry = $registry; + } + + public function build(): array + { + $descriptions = []; + foreach ($this->registry->priceTypes() as $priceType) { + $descriptions[] = $priceType->documentRepresentation(); + } + + return $descriptions; + } +} diff --git a/src/product/ParentNodeDefinitionInterface.php b/src/product/ParentNodeDefinitionInterface.php new file mode 100644 index 00000000..ffeecf3c --- /dev/null +++ b/src/product/ParentNodeDefinitionInterface.php @@ -0,0 +1,12 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product; + +use hiqdev\php\billing\product\behavior\BehaviorCollectionInterface; + +interface ParentNodeDefinitionInterface +{ + public function withBehaviors(): BehaviorCollectionInterface; + + public function hasBehavior(string $behaviorClassName): bool; +} \ No newline at end of file diff --git a/src/product/ProductInterface.php b/src/product/ProductInterface.php new file mode 100644 index 00000000..684faa06 --- /dev/null +++ b/src/product/ProductInterface.php @@ -0,0 +1,10 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product; + +interface ProductInterface +{ + public function toProductName(): string; + + public function label(): string; +} diff --git a/src/product/ProductNotDefinedException.php b/src/product/ProductNotDefinedException.php new file mode 100644 index 00000000..c3ba742c --- /dev/null +++ b/src/product/ProductNotDefinedException.php @@ -0,0 +1,10 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product; + +use hiqdev\php\billing\Exception\LogicException; + +class ProductNotDefinedException extends LogicException +{ + +} diff --git a/src/product/TariffTypeDefinition.php b/src/product/TariffTypeDefinition.php new file mode 100644 index 00000000..1b5d29d7 --- /dev/null +++ b/src/product/TariffTypeDefinition.php @@ -0,0 +1,77 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product; + +use hiqdev\php\billing\product\behavior\BehaviorTariffTypeCollection; +use hiqdev\php\billing\product\Domain\Model\TariffTypeInterface; +use hiqdev\php\billing\product\price\PriceTypeDefinitionCollection; +use hiqdev\php\billing\product\price\PriceTypeDefinitionFactory; + +class TariffTypeDefinition implements TariffTypeDefinitionInterface +{ + private ?ProductInterface $product = null; + + private PriceTypeDefinitionCollection $prices; + + private BehaviorTariffTypeCollection $behaviorCollection; + + public function __construct(private readonly TariffTypeInterface $tariffType) + { + $this->prices = new PriceTypeDefinitionCollection($this, new PriceTypeDefinitionFactory()); + $this->behaviorCollection = new BehaviorTariffTypeCollection($this, $tariffType); + } + + public function tariffType(): TariffTypeInterface + { + return $this->tariffType; + } + + public function ofProduct(ProductInterface $product): TariffTypeDefinitionInterface + { + $this->product = $product; + + return $this; + } + + public function getProduct(): ProductInterface + { + if ($this->product === null) { + throw new ProductNotDefinedException('Product is not set. Call the ofProduct() method first.'); + } + + return $this->product; + } + + public function setPricesSuggester(string $suggesterClass): TariffTypeDefinitionInterface + { + // Validate or store the suggester class + return $this; + } + + public function withPrices(): PriceTypeDefinitionCollection + { + return $this->prices; + } + + public function withBehaviors(): BehaviorTariffTypeCollection + { + return $this->behaviorCollection; + } + + public function hasBehavior(string $behaviorClassName): bool + { + foreach ($this->behaviorCollection as $behavior) { + if ($behavior instanceof $behaviorClassName) { + return true; + } + } + + return false; + } + + public function end(): TariffTypeDefinitionInterface + { + // Validate the TariffType and lock its state + return $this; + } +} diff --git a/src/product/TariffTypeDefinitionFactory.php b/src/product/TariffTypeDefinitionFactory.php new file mode 100644 index 00000000..ff8bf398 --- /dev/null +++ b/src/product/TariffTypeDefinitionFactory.php @@ -0,0 +1,13 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product; + +use hiqdev\php\billing\product\Domain\Model\TariffTypeInterface; + +class TariffTypeDefinitionFactory +{ + public static function create(TariffTypeInterface $tariffType): TariffTypeDefinition + { + return new TariffTypeDefinition($tariffType); + } +} diff --git a/src/product/TariffTypeDefinitionInterface.php b/src/product/TariffTypeDefinitionInterface.php new file mode 100644 index 00000000..366610f1 --- /dev/null +++ b/src/product/TariffTypeDefinitionInterface.php @@ -0,0 +1,28 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product; + +use hiqdev\php\billing\product\Domain\Model\TariffTypeInterface; +use hiqdev\php\billing\product\price\PriceTypeDefinitionCollectionInterface; + +/** + * @template T of PriceTypeDefinitionCollectionInterface + */ +interface TariffTypeDefinitionInterface extends ParentNodeDefinitionInterface +{ + public function tariffType(): TariffTypeInterface; + + public function ofProduct(ProductInterface $product): self; + + public function getProduct(): ProductInterface; + + public function setPricesSuggester(string $suggesterClass): self; + + /** + * @return PriceTypeDefinitionCollectionInterface + * @psalm-return T + */ + public function withPrices(): PriceTypeDefinitionCollectionInterface; + + public function end(): static; +} diff --git a/src/product/behavior/BehaviorCollection.php b/src/product/behavior/BehaviorCollection.php new file mode 100644 index 00000000..33820c7f --- /dev/null +++ b/src/product/behavior/BehaviorCollection.php @@ -0,0 +1,29 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\behavior; + +use hiqdev\php\billing\product\Domain\Model\TariffTypeInterface; + +class BehaviorCollection implements BehaviorCollectionInterface +{ + /** @var BehaviorInterface[] */ + private array $behaviors = []; + + public function __construct(private readonly TariffTypeInterface $tariffType) + { + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->behaviors); + } + + public function attach(BehaviorInterface $behavior): self + { + $behavior->setTariffType($this->tariffType); + + $this->behaviors[] = $behavior; + + return $this; + } +} diff --git a/src/product/behavior/BehaviorCollectionInterface.php b/src/product/behavior/BehaviorCollectionInterface.php new file mode 100644 index 00000000..26f6b1ac --- /dev/null +++ b/src/product/behavior/BehaviorCollectionInterface.php @@ -0,0 +1,13 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\behavior; + +interface BehaviorCollectionInterface extends \IteratorAggregate +{ + /** + * @return BehaviorInterface[] + */ + public function getIterator(): \Traversable; + + public function attach(BehaviorInterface $behavior): self; +} diff --git a/src/product/behavior/BehaviorInterface.php b/src/product/behavior/BehaviorInterface.php new file mode 100644 index 00000000..9d00c1f2 --- /dev/null +++ b/src/product/behavior/BehaviorInterface.php @@ -0,0 +1,15 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\behavior; + +use hiqdev\php\billing\product\Domain\Model\TariffTypeInterface; + +/** + * Empty interface for mark product behavior + */ +interface BehaviorInterface +{ + public function setTariffType(TariffTypeInterface $tariffTypeName): void; + + public function getTariffType(): TariffTypeInterface; +} diff --git a/src/product/behavior/BehaviorNotFoundException.php b/src/product/behavior/BehaviorNotFoundException.php new file mode 100644 index 00000000..bf8fffc7 --- /dev/null +++ b/src/product/behavior/BehaviorNotFoundException.php @@ -0,0 +1,9 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\behavior; + +use hiqdev\php\billing\Exception\RuntimeException; + +class BehaviorNotFoundException extends RuntimeException +{ +} diff --git a/src/product/behavior/BehaviorPriceTypeDefinitionCollection.php b/src/product/behavior/BehaviorPriceTypeDefinitionCollection.php new file mode 100644 index 00000000..b4c743c1 --- /dev/null +++ b/src/product/behavior/BehaviorPriceTypeDefinitionCollection.php @@ -0,0 +1,19 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\behavior; + +use hiqdev\php\billing\product\Domain\Model\TariffTypeInterface; +use hiqdev\php\billing\product\price\PriceTypeDefinition; + +class BehaviorPriceTypeDefinitionCollection extends BehaviorCollection +{ + public function __construct(private readonly PriceTypeDefinition $parent, TariffTypeInterface $tariffType) + { + parent::__construct($tariffType); + } + + public function end(): PriceTypeDefinition + { + return $this->parent; + } +} diff --git a/src/product/behavior/BehaviorTariffTypeCollection.php b/src/product/behavior/BehaviorTariffTypeCollection.php new file mode 100644 index 00000000..0d4b5de2 --- /dev/null +++ b/src/product/behavior/BehaviorTariffTypeCollection.php @@ -0,0 +1,19 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\behavior; + +use hiqdev\php\billing\product\Domain\Model\TariffTypeInterface; +use hiqdev\php\billing\product\TariffTypeDefinition; + +class BehaviorTariffTypeCollection extends BehaviorCollection +{ + public function __construct(private readonly TariffTypeDefinition $parent, TariffTypeInterface $tariffType) + { + parent::__construct($tariffType); + } + + public function end(): TariffTypeDefinition + { + return $this->parent; + } +} diff --git a/src/product/behavior/InvalidBehaviorException.php b/src/product/behavior/InvalidBehaviorException.php new file mode 100644 index 00000000..82ed5868 --- /dev/null +++ b/src/product/behavior/InvalidBehaviorException.php @@ -0,0 +1,9 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\behavior; + +use InvalidArgumentException; + +class InvalidBehaviorException extends InvalidArgumentException +{ +} diff --git a/src/product/invoice/InvalidRepresentationException.php b/src/product/invoice/InvalidRepresentationException.php new file mode 100644 index 00000000..605deb94 --- /dev/null +++ b/src/product/invoice/InvalidRepresentationException.php @@ -0,0 +1,10 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\invoice; + +use InvalidArgumentException; + +class InvalidRepresentationException extends InvalidArgumentException +{ + +} diff --git a/src/product/invoice/InvoiceRepresentationCollection.php b/src/product/invoice/InvoiceRepresentationCollection.php new file mode 100644 index 00000000..856ecaf9 --- /dev/null +++ b/src/product/invoice/InvoiceRepresentationCollection.php @@ -0,0 +1,47 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\invoice; + +use hiqdev\php\billing\product\price\PriceTypeDefinition; + +/** + * @template T of PriceTypeDefinition + */ +class InvoiceRepresentationCollection implements \IteratorAggregate +{ + private array $representations = []; + + public function __construct(private readonly PriceTypeDefinition $priceTypeDefinition) + { + } + + /** + * @return RepresentationInterface[] + */ + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->representations); + } + + public function attach(RepresentationInterface $representation): self + { + $representation->setType($this->priceTypeDefinition->type()); + + $this->representations[] = $representation; + + return $this; + } + + /** + * @psalm-return T + */ + public function end(): PriceTypeDefinition + { + return $this->priceTypeDefinition; + } + + public function filterByType(string $className): array + { + return array_filter($this->representations, fn($r) => $r instanceof $className); + } +} diff --git a/src/product/invoice/RepresentationInterface.php b/src/product/invoice/RepresentationInterface.php new file mode 100644 index 00000000..27b71ba6 --- /dev/null +++ b/src/product/invoice/RepresentationInterface.php @@ -0,0 +1,14 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\invoice; + +use hiqdev\php\billing\type\TypeInterface; + +interface RepresentationInterface +{ + public function getSql(): string; + + public function getType(): TypeInterface; + + public function setType(TypeInterface $type): void; +} diff --git a/src/product/price/PriceTypeDefinition.php b/src/product/price/PriceTypeDefinition.php new file mode 100644 index 00000000..e17b5ce4 --- /dev/null +++ b/src/product/price/PriceTypeDefinition.php @@ -0,0 +1,182 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\price; + +use hiqdev\php\billing\product\AggregateInterface; +use hiqdev\php\billing\product\AggregateNotDefinedException; +use hiqdev\php\billing\product\behavior\BehaviorPriceTypeDefinitionCollection; +use hiqdev\php\billing\product\invoice\InvoiceRepresentationCollection; +use hiqdev\php\billing\product\ParentNodeDefinitionInterface; +use hiqdev\php\billing\product\quantity\InvalidQuantityFormatterException; +use hiqdev\php\billing\product\quantity\QuantityFormatterDefinition; +use hiqdev\php\billing\product\quantity\QuantityFormatterFactory; +use hiqdev\php\billing\product\quantity\FractionQuantityData; +use hiqdev\php\billing\product\Domain\Model\TariffTypeInterface; +use hiqdev\php\billing\product\Domain\Model\Unit\FractionUnitInterface; +use hiqdev\php\billing\product\Domain\Model\Unit\UnitInterface; +use hiqdev\php\billing\product\quantity\QuantityFormatterInterface; +use hiqdev\php\billing\type\TypeInterface; + +/** + * @template T of PriceTypeDefinitionCollectionInterface + * @psalm-consistent-templates + */ +class PriceTypeDefinition implements ParentNodeDefinitionInterface +{ + private UnitInterface $unit; + + private string $description; + + private QuantityFormatterDefinition $quantityFormatterDefinition; + + private InvoiceRepresentationCollection $invoiceCollection; + + private BehaviorPriceTypeDefinitionCollection $behaviorCollection; + + private ?AggregateInterface $aggregate = null; + + public function __construct( + /** + * @psalm-var T + */ + private readonly PriceTypeDefinitionCollectionInterface $parent, + private readonly TypeInterface $type, + TariffTypeInterface $tariffType, + ) { + $this->invoiceCollection = new InvoiceRepresentationCollection($this); + $this->behaviorCollection = new BehaviorPriceTypeDefinitionCollection($this, $tariffType); + + $this->init(); + } + + protected function init(): void + { + // Hook + } + + public function unit(UnitInterface $unit): self + { + $this->unit = $unit; + + return $this; + } + + public function description(string $description): self + { + $this->description = $description; + + return $this; + } + + public function getDescription(): string + { + return $this->description; + } + + /** + * @param string $formatterClass + * @param null|FractionUnitInterface|string $fractionUnit + * @return $this + * @throws InvalidQuantityFormatterException + */ + public function quantityFormatter(string $formatterClass, $fractionUnit = null): self + { + if (!\class_exists($formatterClass)) { + throw new InvalidQuantityFormatterException("Formatter class $formatterClass does not exist"); + } + + $this->quantityFormatterDefinition = new QuantityFormatterDefinition($formatterClass, $fractionUnit); + + return $this; + } + + public function createQuantityFormatter( + FractionQuantityData $data, + ): QuantityFormatterInterface { + return QuantityFormatterFactory::create( + $this->getUnit()->createExternalUnit(), + $this->quantityFormatterDefinition, + $data, + ); + } + + /** + * @psalm-return T + */ + public function end(): PriceTypeDefinitionCollectionInterface + { + // Validate the PriceType and lock its state + return $this->parent; + } + + /** + * @psalm-return InvoiceRepresentationCollection<self> + */ + public function documentRepresentation(): InvoiceRepresentationCollection + { + return $this->invoiceCollection; + } + + public function measuredWith(\hiqdev\billing\registry\measure\RcpTrafCollector $param): self + { + return $this; + } + + public function type(): TypeInterface + { + return $this->type; + } + + public function hasType(TypeInterface $type): bool + { + return $this->type->equals($type); + } + + public function getUnit(): UnitInterface + { + return $this->unit; + } + + public function withBehaviors(): BehaviorPriceTypeDefinitionCollection + { + return $this->behaviorCollection; + } + + public function hasBehavior(string $behaviorClassName): bool + { + foreach ($this->behaviorCollection as $behavior) { + if ($behavior instanceof $behaviorClassName) { + return true; + } + } + + return false; + } + + /** + * це параметер визначає агрегатну функцію яка застосовується для щоденно записаних ресурсів щоб визнизначти + * місячне споживання за яке потрібно пробілити клієнта + * + * @param AggregateInterface $aggregate + * @return self + */ + public function aggregation(AggregateInterface $aggregate): self + { + $this->aggregate = $aggregate; + + return $this; + } + + /** + * @return AggregateInterface + * @throws AggregateNotDefinedException + */ + public function getAggregate(): AggregateInterface + { + if ($this->aggregate === null) { + throw new AggregateNotDefinedException('Aggregate is not set. Call the aggregation() method first.'); + } + + return $this->aggregate; + } +} diff --git a/src/product/price/PriceTypeDefinitionCollection.php b/src/product/price/PriceTypeDefinitionCollection.php new file mode 100644 index 00000000..a3cc854f --- /dev/null +++ b/src/product/price/PriceTypeDefinitionCollection.php @@ -0,0 +1,52 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\price; + +use hiqdev\php\billing\product\TariffTypeDefinitionInterface; +use hiqdev\php\billing\type\TypeInterface; + +/** + * @template T of PriceTypeDefinitionCollectionInterface + * @template M of TariffTypeDefinitionInterface + * @mixin T + */ +class PriceTypeDefinitionCollection implements PriceTypeDefinitionCollectionInterface +{ + private PriceTypeStorage $storage; + + private PriceTypeDefinitionCollectionInterface $collectionInstance; + + public function __construct( + private readonly TariffTypeDefinitionInterface $parent, + private readonly PriceTypeDefinitionFactoryInterface $factory, + PriceTypeDefinitionCollectionInterface $collectionInstance = null, + ) { + $this->storage = new PriceTypeStorage(); + $this->collectionInstance = $collectionInstance ?? $this; + } + + /** + * @return PriceTypeDefinition[] + */ + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->storage->getAll()); + } + + public function priceType(TypeInterface $type): PriceTypeDefinition + { + $priceType = $this->factory->create($this->collectionInstance, $type, $this->parent->tariffType()); + $this->storage->add($type, $priceType); + + return $priceType; + } + + /** + * @return TariffTypeDefinitionInterface + * @plsam-return M + */ + public function end(): TariffTypeDefinitionInterface + { + return $this->parent; + } +} diff --git a/src/product/price/PriceTypeDefinitionCollectionInterface.php b/src/product/price/PriceTypeDefinitionCollectionInterface.php new file mode 100644 index 00000000..2f258aab --- /dev/null +++ b/src/product/price/PriceTypeDefinitionCollectionInterface.php @@ -0,0 +1,18 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\price; + +use hiqdev\php\billing\product\TariffTypeDefinitionInterface; +use hiqdev\php\billing\type\TypeInterface; + +interface PriceTypeDefinitionCollectionInterface extends \IteratorAggregate +{ + /** + * @return PriceTypeDefinition[] + */ + public function getIterator(): \Traversable; + + public function priceType(TypeInterface $type): PriceTypeDefinition; + + public function end(): TariffTypeDefinitionInterface; +} \ No newline at end of file diff --git a/src/product/price/PriceTypeDefinitionFactory.php b/src/product/price/PriceTypeDefinitionFactory.php new file mode 100644 index 00000000..34b813f4 --- /dev/null +++ b/src/product/price/PriceTypeDefinitionFactory.php @@ -0,0 +1,17 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\price; + +use hiqdev\php\billing\product\Domain\Model\TariffTypeInterface; +use hiqdev\php\billing\type\TypeInterface; + +class PriceTypeDefinitionFactory implements PriceTypeDefinitionFactoryInterface +{ + public function create( + PriceTypeDefinitionCollectionInterface $parent, + TypeInterface $type, + TariffTypeInterface $tariffType, + ): PriceTypeDefinition { + return new PriceTypeDefinition($parent, $type, $tariffType); + } +} diff --git a/src/product/price/PriceTypeDefinitionFactoryInterface.php b/src/product/price/PriceTypeDefinitionFactoryInterface.php new file mode 100644 index 00000000..e3eababe --- /dev/null +++ b/src/product/price/PriceTypeDefinitionFactoryInterface.php @@ -0,0 +1,15 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\price; + +use hiqdev\php\billing\product\Domain\Model\TariffTypeInterface; +use hiqdev\php\billing\type\TypeInterface; + +interface PriceTypeDefinitionFactoryInterface +{ + public function create( + PriceTypeDefinitionCollectionInterface $parent, + TypeInterface $type, + TariffTypeInterface $tariffType, + ): PriceTypeDefinition; +} diff --git a/src/product/price/PriceTypeInterface.php b/src/product/price/PriceTypeInterface.php new file mode 100644 index 00000000..5fddfc52 --- /dev/null +++ b/src/product/price/PriceTypeInterface.php @@ -0,0 +1,8 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\price; + +interface PriceTypeInterface +{ + public function name(): string; +} \ No newline at end of file diff --git a/src/product/price/PriceTypeStorage.php b/src/product/price/PriceTypeStorage.php new file mode 100644 index 00000000..9fa96329 --- /dev/null +++ b/src/product/price/PriceTypeStorage.php @@ -0,0 +1,30 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\price; + +use hiqdev\php\billing\type\TypeInterface; + +class PriceTypeStorage +{ + private array $pricesGroupedByPriceType = []; + + public function add(TypeInterface $type, PriceTypeDefinition $priceTypeDefinition): void + { + $this->pricesGroupedByPriceType[$type->getName()][] = $priceTypeDefinition; + } + + /** + * @return PriceTypeDefinition[] + */ + public function getAll(): array + { + $allPrices = []; + foreach ($this->pricesGroupedByPriceType as $prices) { + foreach ($prices as $price) { + $allPrices[] = $price; + } + } + + return $allPrices; + } +} diff --git a/src/product/quantity/FractionQuantityData.php b/src/product/quantity/FractionQuantityData.php new file mode 100644 index 00000000..5f6d409e --- /dev/null +++ b/src/product/quantity/FractionQuantityData.php @@ -0,0 +1,14 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\quantity; + +use hiqdev\php\units\Quantity; + +final class FractionQuantityData +{ + public function __construct( + public readonly Quantity $quantity, + public readonly string $time, + public readonly ?float $fractionOfMonth + ) {} +} diff --git a/src/product/quantity/InvalidQuantityFormatterException.php b/src/product/quantity/InvalidQuantityFormatterException.php new file mode 100644 index 00000000..ff5d7ecc --- /dev/null +++ b/src/product/quantity/InvalidQuantityFormatterException.php @@ -0,0 +1,10 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\quantity; + +use InvalidArgumentException; + +class InvalidQuantityFormatterException extends InvalidArgumentException +{ + +} \ No newline at end of file diff --git a/src/product/quantity/QuantityFormatterDefinition.php b/src/product/quantity/QuantityFormatterDefinition.php new file mode 100644 index 00000000..45c6fca2 --- /dev/null +++ b/src/product/quantity/QuantityFormatterDefinition.php @@ -0,0 +1,29 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\quantity; + +use hiqdev\php\billing\product\Domain\Model\Unit\FractionUnitInterface; + +class QuantityFormatterDefinition +{ + private string $formatterClass; + + /** @var FractionUnitInterface|null|string */ + private $fractionUnit; + + public function __construct(string $formatterClass, $fractionUnit = null) + { + $this->formatterClass = $formatterClass; + $this->fractionUnit = $fractionUnit; + } + + public function formatterClass(): string + { + return $this->formatterClass; + } + + public function getFractionUnit() + { + return $this->fractionUnit; + } +} diff --git a/src/product/quantity/QuantityFormatterFactory.php b/src/product/quantity/QuantityFormatterFactory.php new file mode 100644 index 00000000..2d7c5d4f --- /dev/null +++ b/src/product/quantity/QuantityFormatterFactory.php @@ -0,0 +1,18 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\quantity; + +use hiqdev\php\units\UnitInterface; + +class QuantityFormatterFactory +{ + public static function create( + UnitInterface $unit, + QuantityFormatterDefinition $definition, + FractionQuantityData $data, + ): QuantityFormatterInterface { + $formatterClass = $definition->formatterClass(); + + return new $formatterClass($unit, $definition->getFractionUnit(), $data); + } +} diff --git a/src/product/quantity/QuantityFormatterInterface.php b/src/product/quantity/QuantityFormatterInterface.php new file mode 100644 index 00000000..88f5a257 --- /dev/null +++ b/src/product/quantity/QuantityFormatterInterface.php @@ -0,0 +1,28 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\quantity; + +interface QuantityFormatterInterface +{ + /** + * Returns textual user friendly representation of the quantity. + * E.g. 20 days, 30 GB, 1 year. + * + * @return string + */ + public function format(): string; + + /** + * Returns numeric to be saved in DB. + * + * @return string + */ + public function getValue(): string; + + /** + * Returns numeric user friendly representation of the quantity. + * + * @return string + */ + public function getClientValue(): string; +} \ No newline at end of file diff --git a/src/product/quantity/QuantityFormatterNotFoundException.php b/src/product/quantity/QuantityFormatterNotFoundException.php new file mode 100644 index 00000000..d97076d7 --- /dev/null +++ b/src/product/quantity/QuantityFormatterNotFoundException.php @@ -0,0 +1,9 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\product\quantity; + +use RuntimeException; + +class QuantityFormatterNotFoundException extends RuntimeException +{ +} diff --git a/tests/unit/product/TariffTypeTest.php b/tests/unit/product/TariffTypeTest.php new file mode 100644 index 00000000..dd047513 --- /dev/null +++ b/tests/unit/product/TariffTypeTest.php @@ -0,0 +1,77 @@ +<?php declare(strict_types=1); + +namespace hiqdev\php\billing\tests\unit\product; + +use hiqdev\php\billing\product\PriceTypesCollection; +use hiqdev\php\billing\product\TariffType; +use PHPUnit\Framework\TestCase; + +class TariffTypeTest extends TestCase +{ + public function testTariffTypeInitialization(): void + { + $tariffType = new TariffType('server'); + + $this->assertSame('server', $this->getPrivateProperty($tariffType, 'name'), 'TariffType name should be initialized correctly.'); + $this->assertInstanceOf(PriceTypesCollection::class, $this->getPrivateProperty($tariffType, 'prices'), 'Prices should be an instance of PriceTypesCollection.'); + } + + public function testOfProduct(): void + { + $tariffType = new TariffType('server'); + $tariffType->ofProduct('ServerProductClass'); + + $this->assertSame( + 'ServerProductClass', + $this->getPrivateProperty($tariffType, 'productClass'), + 'Product class should be set correctly.' + ); + } + + public function testAttachBehavior(): void + { + $tariffType = new TariffType('server'); + $behavior = new OncePerMonthPlanChangeBehavior(); + $tariffType->attach($behavior); + + $behaviors = $this->getPrivateProperty($tariffType, 'behaviors'); + + $this->assertCount(1, $behaviors, 'Behavior should be added to the behaviors list.'); + $this->assertSame($behavior, $behaviors[0], 'Behavior should match the attached instance.'); + } + + public function testPricesCollectionInteraction(): void + { + $tariffType = new TariffType('server'); + $prices = $tariffType->withPrices(); + + $this->assertInstanceOf(PriceTypesCollection::class, $prices, 'withPrices() should return a PriceTypesCollection instance.'); + + $priceType = $prices->monthly('support_time'); + $priceType->unit('hour')->description('Monthly fee for support time'); + $priceType->end(); + + $this->assertNotEmpty($this->getPrivateProperty($prices, 'prices'), 'PriceTypesCollection should contain defined price types.'); + } + + public function testEndLocksTariffType(): void + { + $tariffType = new TariffType('server'); + $tariffType->end(); + + // Assuming TariffType has a `locked` private property + $isLocked = $this->getPrivateProperty($tariffType, 'locked'); + $this->assertTrue($isLocked, 'TariffType should be locked after calling end().'); + } + + /** + * Helper function to access private properties for testing. + */ + private function getPrivateProperty($object, $propertyName) + { + $reflection = new \ReflectionClass($object); + $property = $reflection->getProperty($propertyName); + $property->setAccessible(true); + return $property->getValue($object); + } +}