This commit is contained in:
Mateusz 2026-06-26 07:18:35 +00:00 committed by GitHub
commit 4a8ffaa2dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 176 additions and 15 deletions

View file

@ -56,3 +56,48 @@ Feature: Receiving discount based on total of items from specific taxon
When I check the details of my cart
Then my cart total should be "$125.00"
And my discount should be "-$15.00"
@api @ui
Scenario: Receiving discount on order while buying product from child taxon of promoted taxon when child taxons are included
Given the "T-Shirts" taxon has child taxon "Polo T-Shirts"
And the store has a product "Polo T-Shirt" priced at "$100.00"
And it belongs to "Polo T-Shirts"
And the promotion gives "$20.00" off if order contains products classified as "T-Shirts" with a minimum value of "$50.00", including child taxons
And I added product "Polo T-Shirt" to the cart
When I check the details of my cart
Then my cart total should be "$80.00"
And my discount should be "-$20.00"
@api @ui
Scenario: Receiving no discount on order while buying product from child taxon of promoted taxon which not fits price criteria
Given the "T-Shirts" taxon has child taxon "Polo T-Shirts"
And the store has a product "Polo T-Shirt" priced at "$30.00"
And it belongs to "Polo T-Shirts"
And the promotion gives "$20.00" off if order contains products classified as "T-Shirts" with a minimum value of "$50.00", including child taxons
And I added product "Polo T-Shirt" to the cart
When I check the details of my cart
Then my cart total should be "$30.00"
And there should be no discount applied
@api @ui
Scenario: Receiving discount on order while buying products from both parent and child taxon which fit price criteria
Given the "T-Shirts" taxon has child taxon "Polo T-Shirts"
And the store has a product "Polo T-Shirt" priced at "$60.00"
And it belongs to "Polo T-Shirts"
And the promotion gives "$10.00" off if order contains products classified as "T-Shirts" with a minimum value of "$50.00", including child taxons
And I added product "PHP T-Shirt" to the cart
And I added product "Polo T-Shirt" to the cart
When I check the details of my cart
Then my cart total should be "$150.00"
And my discount should be "-$10.00"
@api @ui
Scenario: Receiving no discount on order while buying product from child taxon when child taxons are excluded from the rule
Given the "T-Shirts" taxon has child taxon "Polo T-Shirts"
And the store has a product "Polo T-Shirt" priced at "$100.00"
And it belongs to "Polo T-Shirts"
And the promotion gives "$20.00" off if order contains products classified as "T-Shirts" with a minimum value of "$50.00"
And I added product "Polo T-Shirt" to the cart
When I check the details of my cart
Then my cart total should be "$100.00"
And there should be no discount applied

View file

@ -635,6 +635,19 @@ final readonly class PromotionContext implements Context
$this->createFixedPromotion($promotion, $discount, [], $rule);
}
#[Given('/^([^"]+) gives ("(?:€|£|\$)[^"]+") off if order contains products (classified as "[^"]+") with a minimum value of ("(?:€|£|\$)[^"]+"), including child taxons$/')]
public function thePromotionGivesOffIfOrderContainsProductsClassifiedAsAndPricedAtIncludingChildTaxons(
PromotionInterface $promotion,
int $discount,
TaxonInterface $taxon,
int $amount,
): void {
$channelCode = $this->getChannelCode();
$rule = $this->ruleFactory->createItemsFromTaxonTotal($channelCode, $taxon->getCode(), $amount, true);
$this->createFixedPromotion($promotion, $discount, [], $rule);
}
#[Given('/^([^"]+) gives ("(?:€|£|\$)[^"]+") off customer\'s (\d)(?:st|nd|rd|th) order$/')]
public function itGivesFixedOffCustomersNthOrder(PromotionInterface $promotion, int $discount, int $nth): void
{

View file

@ -19,6 +19,7 @@ use Sylius\Bundle\TaxonomyBundle\Form\Type\TaxonAutocompleteChoiceType;
use Sylius\Component\Core\Model\TaxonInterface;
use Sylius\Resource\Doctrine\Persistence\RepositoryInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\ReversedTransformer;
use Symfony\Component\OptionsResolver\OptionsResolver;
@ -40,6 +41,10 @@ final class TotalOfItemsFromTaxonConfigurationType extends AbstractType
'label' => 'sylius.form.promotion_rule.total_of_items_from_taxon.amount',
'currency' => $options['currency'],
])
->add('include_child_taxons', CheckboxType::class, [
'label' => 'sylius.form.promotion_rule.total_of_items_from_taxon.include_child_taxons',
'required' => false,
])
;
$builder->get('taxon')->addModelTransformer(

View file

@ -205,8 +205,9 @@ sylius:
product:
products: Products
total_of_items_from_taxon:
taxon: Taxon
amount: Amount
include_child_taxons: Include products from child taxons
taxon: Taxon
payment_method:
channels: Channels
product_attribute:

View file

@ -54,13 +54,16 @@ final class PromotionRuleFactory implements PromotionRuleFactoryInterface
return $this->createPromotionRule(HasTaxonRuleChecker::TYPE, ['taxons' => $taxons]);
}
public function createItemsFromTaxonTotal(string $channelCode, string $taxonCode, int $amount): PromotionRuleInterface
{
public function createItemsFromTaxonTotal(
string $channelCode,
string $taxonCode,
int $amount,
bool $includeChildTaxons = false,
): PromotionRuleInterface {
return $this->createPromotionRule(
TotalOfItemsFromTaxonRuleChecker::TYPE,
[$channelCode => ['taxon' => $taxonCode, 'amount' => $amount]],
)
;
[$channelCode => ['amount' => $amount, 'include_child_taxons' => $includeChildTaxons, 'taxon' => $taxonCode]],
);
}
public function createNthOrder(int $nth): PromotionRuleInterface

View file

@ -15,6 +15,7 @@ namespace Sylius\Component\Core\Promotion\Checker\Rule;
use Sylius\Component\Core\Model\OrderInterface;
use Sylius\Component\Core\Model\OrderItemInterface;
use Sylius\Component\Core\Model\ProductInterface;
use Sylius\Component\Core\Model\TaxonInterface;
use Sylius\Component\Promotion\Checker\Rule\RuleCheckerInterface;
use Sylius\Component\Promotion\Exception\UnsupportedTypeException;
@ -56,15 +57,43 @@ final class TotalOfItemsFromTaxonRuleChecker implements RuleCheckerInterface
return false;
}
$includeChildTaxons = $configuration['include_child_taxons'] ?? false;
$itemsWithTaxonTotal = 0;
/** @var OrderItemInterface $item */
foreach ($subject->getItems() as $item) {
if ($item->getProduct()->hasTaxon($targetTaxon)) {
$belongs = $includeChildTaxons
? $this->productBelongsToTaxonOrDescendant($item->getProduct(), $targetTaxon)
: $this->productBelongsToTaxon($item->getProduct(), $targetTaxon);
if ($belongs) {
$itemsWithTaxonTotal += $item->getTotal();
}
}
return $itemsWithTaxonTotal >= $configuration['amount'];
}
private function productBelongsToTaxon(?ProductInterface $product, TaxonInterface $targetTaxon): bool
{
if ($product === null) {
return false;
}
return $product->getTaxons()->contains($targetTaxon);
}
private function productBelongsToTaxonOrDescendant(?ProductInterface $product, TaxonInterface $targetTaxon): bool
{
if ($product === null) {
return false;
}
foreach ($product->getTaxons() as $taxon) {
if ($taxon === $targetTaxon || $taxon->getAncestors()->contains($targetTaxon)) {
return true;
}
}
return false;
}
}

View file

@ -84,11 +84,20 @@ final class PromotionRuleFactoryTest extends TestCase
{
$this->decoratedFactory->expects($this->once())->method('createNew')->willReturn($this->rule);
$this->rule->expects($this->once())->method('setType')->with(TotalOfItemsFromTaxonRuleChecker::TYPE);
$this->rule->expects($this->once())->method('setConfiguration')->with(['WEB_US' => ['taxon' => 'spears', 'amount' => 1000]]);
$this->rule->expects($this->once())->method('setConfiguration')->with(['WEB_US' => ['amount' => 1000, 'include_child_taxons' => false, 'taxon' => 'spears']]);
$this->assertSame($this->rule, $this->factory->createItemsFromTaxonTotal('WEB_US', 'spears', 1000));
}
public function testShouldCreateTotalOfItemsFromTaxonRuleWithChildTaxons(): void
{
$this->decoratedFactory->expects($this->once())->method('createNew')->willReturn($this->rule);
$this->rule->expects($this->once())->method('setType')->with(TotalOfItemsFromTaxonRuleChecker::TYPE);
$this->rule->expects($this->once())->method('setConfiguration')->with(['WEB_US' => ['amount' => 1000, 'include_child_taxons' => true, 'taxon' => 'spears']]);
$this->assertSame($this->rule, $this->factory->createItemsFromTaxonTotal('WEB_US', 'spears', 1000, true));
}
public function testShouldCreateNthOrderRule(): void
{
$this->decoratedFactory->expects($this->once())->method('createNew')->willReturn($this->rule);

View file

@ -85,12 +85,12 @@ final class TotalOfItemsFromTaxonRuleCheckerTest extends TestCase
->with(['code' => 'bows'])
->willReturn($this->bows);
$this->compositeBowItem->expects($this->once())->method('getProduct')->willReturn($this->compositeBow);
$this->compositeBow->expects($this->once())->method('hasTaxon')->with($this->bows)->willReturn(true);
$this->compositeBow->expects($this->once())->method('getTaxons')->willReturn(new ArrayCollection([$this->bows]));
$this->compositeBowItem->expects($this->once())->method('getTotal')->willReturn(5000);
$this->longswordItem->expects($this->once())->method('getProduct')->willReturn($this->longsword);
$this->longsword->expects($this->once())->method('hasTaxon')->with($this->bows)->willReturn(false);
$this->longsword->expects($this->once())->method('getTaxons')->willReturn(new ArrayCollection([]));
$this->reflexBowItem->expects($this->once())->method('getProduct')->willReturn($this->reflexBow);
$this->reflexBow->expects($this->once())->method('hasTaxon')->with($this->bows)->willReturn(true);
$this->reflexBow->expects($this->once())->method('getTaxons')->willReturn(new ArrayCollection([$this->bows]));
$this->reflexBowItem->expects($this->once())->method('getTotal')->willReturn(9000);
$this->assertTrue(
@ -112,10 +112,10 @@ final class TotalOfItemsFromTaxonRuleCheckerTest extends TestCase
->with(['code' => 'bows'])
->willReturn($this->bows);
$this->compositeBowItem->expects($this->once())->method('getProduct')->willReturn($this->compositeBow);
$this->compositeBow->expects($this->once())->method('hasTaxon')->with($this->bows)->willReturn(true);
$this->compositeBow->expects($this->once())->method('getTaxons')->willReturn(new ArrayCollection([$this->bows]));
$this->compositeBowItem->expects($this->once())->method('getTotal')->willReturn(5000);
$this->reflexBowItem->expects($this->once())->method('getProduct')->willReturn($this->reflexBow);
$this->reflexBow->expects($this->once())->method('hasTaxon')->with($this->bows)->willReturn(true);
$this->reflexBow->expects($this->once())->method('getTaxons')->willReturn(new ArrayCollection([$this->bows]));
$this->reflexBowItem->expects($this->once())->method('getTotal')->willReturn(5000);
$this->assertTrue(
@ -123,6 +123,62 @@ final class TotalOfItemsFromTaxonRuleCheckerTest extends TestCase
);
}
public function testShouldRecognizeSubjectAsEligibleIfItHasItemsFromChildTaxonOfConfiguredTaxon(): void
{
$recurveBows = $this->createMock(TaxonInterface::class);
$recurveBows->method('getAncestors')->willReturn(new ArrayCollection([$this->bows]));
$recurveBowItem = $this->createMock(OrderItemInterface::class);
$recurveBow = $this->createMock(ProductInterface::class);
$this->order->expects($this->once())->method('getChannel')->willReturn($this->channel);
$this->channel->expects($this->once())->method('getCode')->willReturn('WEB_US');
$this->order->expects($this->once())->method('getItems')->willReturn(new ArrayCollection([
$this->compositeBowItem,
$recurveBowItem,
]));
$this->taxonRepository
->expects($this->once())
->method('findOneBy')
->with(['code' => 'bows'])
->willReturn($this->bows);
$this->compositeBowItem->expects($this->once())->method('getProduct')->willReturn($this->compositeBow);
$this->compositeBow->expects($this->once())->method('getTaxons')->willReturn(new ArrayCollection([$this->bows]));
$this->compositeBowItem->expects($this->once())->method('getTotal')->willReturn(5000);
$recurveBowItem->expects($this->once())->method('getProduct')->willReturn($recurveBow);
$recurveBow->expects($this->once())->method('getTaxons')->willReturn(new ArrayCollection([$recurveBows]));
$recurveBowItem->expects($this->once())->method('getTotal')->willReturn(5000);
$this->assertTrue(
$this->ruleChecker->isEligible($this->order, ['WEB_US' => ['taxon' => 'bows', 'amount' => 10000, 'include_child_taxons' => true]]),
);
}
public function testShouldNotRecognizeSubjectAsEligibleIfItHasItemsOnlyFromChildTaxonWhenChildTaxonsExcluded(): void
{
$recurveBows = $this->createMock(TaxonInterface::class);
$recurveBowItem = $this->createMock(OrderItemInterface::class);
$recurveBow = $this->createMock(ProductInterface::class);
$this->order->expects($this->once())->method('getChannel')->willReturn($this->channel);
$this->channel->expects($this->once())->method('getCode')->willReturn('WEB_US');
$this->order->expects($this->once())->method('getItems')->willReturn(new ArrayCollection([
$recurveBowItem,
]));
$this->taxonRepository
->expects($this->once())
->method('findOneBy')
->with(['code' => 'bows'])
->willReturn($this->bows);
$recurveBowItem->expects($this->once())->method('getProduct')->willReturn($recurveBow);
$recurveBow->expects($this->once())->method('getTaxons')->willReturn(new ArrayCollection([$recurveBows]));
$this->assertFalse(
$this->ruleChecker->isEligible($this->order, ['WEB_US' => ['taxon' => 'bows', 'amount' => 5000, 'include_child_taxons' => false]]),
);
}
public function testShouldNotRecognizeSubjectAsEligibleIfItemsFromRequiredTaxonHasTooLowTotal(): void
{
$this->order->expects($this->once())->method('getChannel')->willReturn($this->channel);
@ -137,10 +193,10 @@ final class TotalOfItemsFromTaxonRuleCheckerTest extends TestCase
->with(['code' => 'bows'])
->willReturn($this->bows);
$this->compositeBowItem->expects($this->once())->method('getProduct')->willReturn($this->compositeBow);
$this->compositeBow->expects($this->once())->method('hasTaxon')->with($this->bows)->willReturn(true);
$this->compositeBow->expects($this->once())->method('getTaxons')->willReturn(new ArrayCollection([$this->bows]));
$this->compositeBowItem->expects($this->once())->method('getTotal')->willReturn(5000);
$this->longswordItem->expects($this->once())->method('getProduct')->willReturn($this->longsword);
$this->longsword->expects($this->once())->method('hasTaxon')->with($this->bows)->willReturn(false);
$this->longsword->expects($this->once())->method('getTaxons')->willReturn(new ArrayCollection([]));
$this->assertFalse(
$this->ruleChecker->isEligible($this->order, ['WEB_US' => ['taxon' => 'bows', 'amount' => 10000]]),