Add include_child_taxons toggle to TotalOfItemsFromTaxon promotion rule

This commit is contained in:
Mateusz 2026-03-06 11:35:51 +01:00
parent c4bc877222
commit 28accafe56
8 changed files with 93 additions and 13 deletions

View file

@ -58,11 +58,11 @@ Feature: Receiving discount based on total of items from specific taxon
And my discount should be "-$15.00"
@api @ui
Scenario: Receiving discount on order while buying product from child taxon of promoted taxon
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"
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"
@ -73,7 +73,7 @@ Feature: Receiving discount based on total of items from specific taxon
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"
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"
@ -84,9 +84,20 @@ Feature: Receiving discount based on total of items from specific taxon
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"
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

@ -57,11 +57,15 @@ final class TotalOfItemsFromTaxonRuleChecker implements RuleCheckerInterface
return false;
}
$includeChildTaxons = $configuration['include_child_taxons'] ?? false;
$itemsWithTaxonTotal = 0;
/** @var OrderItemInterface $item */
foreach ($subject->getItems() as $item) {
if ($this->productBelongsToTaxonOrDescendant($item->getProduct(), $targetTaxon)) {
$belongs = $includeChildTaxons
? $this->productBelongsToTaxonOrDescendant($item->getProduct(), $targetTaxon)
: $this->productBelongsToTaxon($item->getProduct(), $targetTaxon);
if ($belongs) {
$itemsWithTaxonTotal += $item->getTotal();
}
}
@ -69,6 +73,15 @@ final class TotalOfItemsFromTaxonRuleChecker implements RuleCheckerInterface
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) {

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

@ -150,7 +150,32 @@ final class TotalOfItemsFromTaxonRuleCheckerTest extends TestCase
$recurveBowItem->expects($this->once())->method('getTotal')->willReturn(5000);
$this->assertTrue(
$this->ruleChecker->isEligible($this->order, ['WEB_US' => ['taxon' => 'bows', 'amount' => 10000]]),
$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]]),
);
}