diff --git a/src/product/AggregateNotFoundException.php b/src/product/AggregateNotFoundException.php new file mode 100644 index 0000000..2d9c972 --- /dev/null +++ b/src/product/AggregateNotFoundException.php @@ -0,0 +1,9 @@ +behaviors); + } + + public function attach(BehaviorInterface $behavior): self + { + $behavior->setTariffType($this->tariffType); + + $this->behaviors[] = $behavior; + + return $this; + } +} diff --git a/src/product/BehaviorInterface.php b/src/product/BehaviorInterface.php new file mode 100644 index 0000000..36e054b --- /dev/null +++ b/src/product/BehaviorInterface.php @@ -0,0 +1,15 @@ +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 + { + $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, + ): array { + $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 + */ + public function getBehavior(string $type, string $behaviorClassWrapper): BehaviorInterface + { + $type = $this->convertStringTypeToType($type); + + foreach ($this->priceTypes() as $priceTypeDefinition) { + if ($priceTypeDefinition->hasType($type)) { + foreach ($priceTypeDefinition->withBehaviors() as $behavior) { + if ($behavior instanceof $behaviorClassWrapper) { + return $behavior; + } + } + } + } + + throw new BehaviorNotFoundException('Behaviour was not found'); + } + + 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): Aggregate + { + $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 0000000..ed4415b --- /dev/null +++ b/src/product/BillingRegistryInterface.php @@ -0,0 +1,11 @@ +registry = $registry; + } + + public function build(): array + { + $descriptions = []; + foreach ($this->registry->priceTypes() as $priceType) { + $descriptions[] = $priceType->representInvoice(); + } + + return $descriptions; + } +} diff --git a/src/product/ParentNodeDefinitionInterface.php b/src/product/ParentNodeDefinitionInterface.php new file mode 100644 index 0000000..ced0906 --- /dev/null +++ b/src/product/ParentNodeDefinitionInterface.php @@ -0,0 +1,10 @@ +invoiceCollection = new InvoiceRepresentationCollection($this); + $this->behaviorCollection = new PriceTypeDefinitionBehaviourCollection($this, $tariffType); + + $this->init(); + } + + protected function init(): void + { + // Hook + } + + public function unit(Unit $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|FractionUnit|string $fractionUnit + * @return $this + */ + public function quantityFormatter(string $formatterClass, $fractionUnit = null): self + { + // TODO: check if formatterClass exists + $this->quantityFormatterDefinition = new QuantityFormatterDefinition($formatterClass, $fractionUnit); + + return $this; + } + + public function createQuantityFormatter( + FractionQuantityData $data, + ): QuantityFormatterInterface { + return QuantityFormatterFactory::create( + $this->getUnit()->createExternalUnit(), + $this->quantityFormatterDefinition, + $data, + ); + } + + public function end(): PriceTypeDefinitionCollection + { + // Validate the PriceType and lock its state + return $this->parent; + } + + 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(): Unit + { + return $this->unit; + } + + public function withBehaviors(): PriceTypeDefinitionBehaviourCollection + { + return $this->behaviorCollection; + } + + public function hasBehavior(string $behaviorClassName): bool + { + foreach ($this->behaviorCollection as $behavior) { + if ($behavior instanceof $behaviorClassName) { + return true; + } + } + + return false; + } + + /** + * це параметер визначає агрегатну функцію яка застосовується для щоденно записаних ресурсів щоб визнизначти + * місячне споживання за яке потрібно пробілити клієнта + * + * @param Aggregate $aggregate + * @return self + */ + public function aggregation(Aggregate $aggregate): self + { + $this->aggregate = $aggregate; + + return $this; + } + + public function getAggregate(): Aggregate + { + return $this->aggregate; + } +} diff --git a/src/product/PriceTypeDefinitionCollection.php b/src/product/PriceTypeDefinitionCollection.php new file mode 100644 index 0000000..f21fc76 --- /dev/null +++ b/src/product/PriceTypeDefinitionCollection.php @@ -0,0 +1,109 @@ +getAllPrices()); + } + + private function getAllPrices(): array + { + $allPrices = []; + foreach ($this->pricesGroupedByPriceType as $prices) { + foreach ($prices as $price) { + $allPrices[] = $price; + } + } + + return $allPrices; + } + + public function monthly(PriceType $type): PriceTypeDefinition + { + $priceType = $this->createPriceTypeDefinition(GType::monthly, $type, $this->parent->tariffType()); + + $this->addPriceTypeDefinition($type, $priceType); + + return $priceType; + } + + private function addPriceTypeDefinition(PriceType $type, PriceTypeDefinition $priceTypeDefinition): void + { + $this->pricesGroupedByPriceType[$type->name][] = $priceTypeDefinition; + } + + private function createPriceTypeDefinition( + GType $gType, + PriceType $type, + TariffType $tariffType, + ): PriceTypeDefinition { + return PriceTypeDefinitionFactory::create($this, $type, $gType, $tariffType); + } + + public function overuse(PriceType $type): PriceTypeDefinition + { + $priceType = $this->createPriceTypeDefinition(GType::overuse, $type, $this->parent->tariffType()); + + $this->addPriceTypeDefinition($type, $priceType); + + return $priceType; + } + + public function end(): TariffTypeDefinition + { + return $this->parent; + } + + public function feature(PriceType $type): PriceTypeDefinition + { + $priceType = $this->createPriceTypeDefinition(GType::feature, $type, $this->parent->tariffType()); + + $this->addPriceTypeDefinition($type, $priceType); + + return $priceType; + } + + public function domain(PriceType $type): PriceTypeDefinition + { + $priceType = $this->createPriceTypeDefinition(GType::domain, $type, $this->parent->tariffType()); + + $this->addPriceTypeDefinition($type, $priceType); + + return $priceType; + } + + public function certificate(PriceType $type): PriceTypeDefinition + { + $priceType = $this->createPriceTypeDefinition(GType::certificate, $type, $this->parent->tariffType()); + + $this->addPriceTypeDefinition($type, $priceType); + + return $priceType; + } + + public function discount(PriceType $type): PriceTypeDefinition + { + $priceType = $this->createPriceTypeDefinition(GType::discount, $type, $this->parent->tariffType()); + + $this->addPriceTypeDefinition($type, $priceType); + + return $priceType; + } +} diff --git a/src/product/ProductInterface.php b/src/product/ProductInterface.php new file mode 100644 index 0000000..3b146fd --- /dev/null +++ b/src/product/ProductInterface.php @@ -0,0 +1,8 @@ +prices = new PriceTypeDefinitionCollection($this); + $this->behaviorCollection = new TariffTypeBehaviorCollection($this, $tariffType); + } + + public function tariffType(): TariffType + { + return $this->tariffType; + } + + public function ofProduct(Product $product): self + { + $this->product = $product; + + return $this; + } + + public function setPricesSuggester(string $suggesterClass): self + { + // Validate or store the suggester class + return $this; + } + + public function withPrices(): PriceTypeDefinitionCollection + { + return $this->prices; + } + + public function withBehaviors(): TariffTypeBehaviorCollection + { + 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(): self + { + // Validate the TariffType and lock its state + return $this; + } +} diff --git a/src/quantity/QuantityFormatterInterface.php b/src/quantity/QuantityFormatterInterface.php new file mode 100644 index 0000000..4f8c322 --- /dev/null +++ b/src/quantity/QuantityFormatterInterface.php @@ -0,0 +1,28 @@ +ofProduct(ServerProduct::class) + ->setPricesSuggester(\hiapi\legacy\lib\billing\price\suggester\device\ServerPricesSuggester::class) + ->withPrices() + ->monthly('support_time') + ->unit('hour') + ->description('Monthly fee for support time') + ->quantityFormatter(MonthlyQuantityFormatter::class) + ->invoiceRepresentation(function () { + return 'Invoice for support_time (monthly): $100'; + }) + ->documentRepresentation() + ->attach(new InvoiceRepresentation("trunc(quantity::numeric,0)||' of '||trunc(maxquantity::numeric,0)||' Anycast IP-addresses in '||period")) + ->attach(new PaymentRequestRepresentation("'Recommended prepayment '||trunc(quantity::numeric,0)||' of '||trunc(maxquantity::numeric,0)||' Anycast IP-addresses in '||period")) + ->end() + ->end() + ->overuse('support_time') + ->unit('hour') + ->description('Support time overuse') + ->quantityFormatter(HourBasedOveruseQuantityFormatter::class) + ->invoiceRepresentation(function () { + return 'Invoice for support_time (overuse): $50'; + }) + ->end() + ->end() // Returns control to TariffType + ->withBehaviors() + ->attach(new OncePerMonthPlanChangeBehavior()) + ->end(); + + // Create BillingRegistry and add the TariffType + $billingRegistry = new BillingRegistry(); + $billingRegistry->addTariffType($serverTariffType); + $billingRegistry->lock(); + + // Build invoice descriptions + $builder = new InvoiceDescriptionsBuilder($billingRegistry); + $invoiceDescriptions = $builder->build(); + + // Verify the results + $this->assertIsArray($invoiceDescriptions, 'build() should return an array of invoice descriptions.'); + $this->assertCount(2, $invoiceDescriptions, 'There should be 2 invoice descriptions generated.'); + + $this->assertSame( + 'Invoice for support_time (monthly): $100', + $invoiceDescriptions[0], + 'The first description should match the expected monthly invoice description.' + ); + + $this->assertSame( + 'Invoice for support_time (overuse): $50', + $invoiceDescriptions[1], + 'The second description should match the expected overuse invoice description.' + ); + } +} diff --git a/tests/unit/product/TariffTypeTest.php b/tests/unit/product/TariffTypeTest.php new file mode 100644 index 0000000..dd04751 --- /dev/null +++ b/tests/unit/product/TariffTypeTest.php @@ -0,0 +1,77 @@ +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); + } +}