Implementing remaining promotion compatibility strategies:

  • Only with the selected promotions
  • Any promotion except the selected promotions

Issue fork commerce-3010862

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

Oleksiy created an issue. See original summary.

oleksiy’s picture

Status: Active » Needs work
Issue tags: +Needs tests
StatusFileSize
new8.65 KB
kristiaanvandeneynde’s picture

Status: Needs work » Needs review
StatusFileSize
new9.12 KB

Here's a reroll

kristiaanvandeneynde’s picture

StatusFileSize
new2.74 KB
new11.62 KB

Okay, that went green so here is with updated tests.

Status: Needs review » Needs work

The last submitted patch, 4: commerce-3010862-4.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

kristiaanvandeneynde’s picture

Issue tags: -Needs tests

Refinements up for discussion:

  1. Do not list the active promotion in the reference: It would be weird to be incompatible with oneself
  2. Should promotions that you are/aren't incompatible with be listed as dependencies?

Will fix the single failure shortly.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
StatusFileSize
new3.01 KB
new12.55 KB

This fixes the test failure. Could use some input on the 2 points raised in #6

kristiaanvandeneynde’s picture

StatusFileSize
new1.47 KB
new12.5 KB

The entity reference to the (in)compatible promotions should not be translatable... (IMO)

bojanz’s picture

Title: Port remaining strategies from Commerce Discount » Add missing promotion compatibility strategies

Updating title.

Lisa had a good comment in https://www.drupal.org/project/commerce/issues/2762997#comment-12342017:

One option that would be very helpful to me is allowing the compatibility to apply at the product/order item level instead of just to the order as a whole. Instead of just, "Not with any other promotions", you could also select "Not with any other order total promotions" or "Not with any other order item-level promotions".

Example: the store offers quantity discounts as follows:
* 5% off price for item quantity 5-9
* 10% off price for item quantity 10-19
* 15% off price for item quantity 20 or more

And then certain customers have standing discounts:
* John Smith receives 8% off any quantity of any item

And then suppose John Smith also has a coupon for 12% off product X.

He places an order:
2x product A
12x product B
24x product C
15x product X with coupon

With product/order item level compatibility, he could get 8% off product A, 10% off product B, 15% off product C, and 12% off product X. 4 different promotions applied but only one per order item.
---------------

Regarding compatibility settings in general, one concept I've seen in another system is the idea of a "terminating" condition. That provides a little more flexibility than the all-or-none option. A promotion that is flagged as terminating is compatible only with any lower weight conditions.

tonytheferg’s picture

Could this patch include an option to require the criteria of selected discounts rather than merely allowing compatibility? This is a concept I'm chewing on for complex promotions.

kristiaanvandeneynde’s picture

StatusFileSize
new11.98 KB

Reroll, haven't taken #9 into account yet.

gge’s picture

Hi,

Just tried to apply #11 to a Commerce 8.x-2.20 site. The patched applied correctly but when trying to run update.php got this:
Drupal\Core\Entity\Sql\SqlContentEntityStorageException: Table information not available for the 'compatibility_promotions' field. in Drupal\Core\Entity\Sql\DefaultTableMapping->getFieldTableName() (line 391 of core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php).

Thanks.

andriy khomych’s picture

Rerolled #11 patch for the last dev commerce version:

  • Fix install file code sniffer issue.
  • Change update hook number.
andriy khomych’s picture

Rerolled #11 patch for the 8.x-2.21.0 version:

  • Fix install file code sniffer issue.
  • Change update hook number.
artem rudnitskiy’s picture

Hi everyone, I have issue related to missing this new promotion compatibility strategies. I have promotion with 'Not with any other promotions' compatibility strategy. When I added coupon to the cart in the edit form, the first time it added correctly. But the next time when I'm trying to apply any non-coupon related changes, while saving the order, the promotion condition rejected the already added coupon due
to promotion compatibility strategy. Looks like the condition 'Only with the selected promotions' will solve the problem. So it will be grate to see this update in nearest module version if it possible.

flocondetoile’s picture

StatusFileSize
new10.95 KB

Patch #14 reroll against commerce 2.27. I change the number of the hook_update_N() after the last committed in -dev. I wonder how we can manage this kind of hook_update_N() in a patch, because if used on a project, then the next hook_update_N() could not be played because already done with this patch.

flocondetoile’s picture

StatusFileSize
new11.34 KB
new636 bytes

Little fix added about the promotion form (put the new field into the compatibilty details group).

dd_leo’s picture

StatusFileSize
new11.32 KB

Patch #17 is not relevant in Commerce version 2.28.
The problem mentioned in the #16 comment arose.
I changed the hook_update_N () number.

jsacksick’s picture

Any chance to get a patch that applies?

nathaniel’s picture

StatusFileSize
new11.51 KB

reroll

Status: Needs review » Needs work

The last submitted patch, 20: 3010862-20.patch, failed testing. View results

rgnyldz’s picture

Patch seems to apply without any issue bu nothing happens when I click on "Only with the selected promotions" or "Any promotion except the selected promotions"

I'm on drupal 10.0.1, commerce 2.32 and php8.1

Edit: Add log message

Warning: Undefined array key "#type" in Drupal\Core\Form\FormHelper::processStates() (line 211 of /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/Form/FormHelper.php)
#0 /home/keratai2/public_html/xxxxxxxx.com/web/core/includes/bootstrap.inc(158): _drupal_error_handler_real()
#1 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/Form/FormHelper.php(211): _drupal_error_handler()
#2 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/Render/Renderer.php(400): Drupal\Core\Form\FormHelper::processStates()
#3 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/Render/Renderer.php(446): Drupal\Core\Render\Renderer->doRender()
#4 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/Render/Renderer.php(204): Drupal\Core\Render\Renderer->doRender()
#5 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/Template/TwigExtension.php(477): Drupal\Core\Render\Renderer->render()
#6 /home/keratai2/public_html/xxxxxxxx.com/web/sites/default/files/php/twig/63dd68a000571_commerce-promotion-form.h_x3KsDZaKdGiM4mqm3qjrVUvrW/HofUV6_DHYoSi-y2DzRbHbKgxYLndquBwufJzUssGTg.php(43): Drupal\Core\Template\TwigExtension->escapeFilter()
#7 /home/keratai2/public_html/xxxxxxxx.com/vendor/twig/twig/src/Template.php(394): __TwigTemplate_d465138c02272adc00ad0c96ff0d43bc->doDisplay()
#8 /home/keratai2/public_html/xxxxxxxx.com/vendor/twig/twig/src/Template.php(367): Twig\Template->displayWithErrorHandling()
#9 /home/keratai2/public_html/xxxxxxxx.com/vendor/twig/twig/src/Template.php(379): Twig\Template->display()
#10 /home/keratai2/public_html/xxxxxxxx.com/vendor/twig/twig/src/TemplateWrapper.php(40): Twig\Template->render()
#11 /home/keratai2/public_html/xxxxxxxx.com/web/core/themes/engines/twig/twig.engine(53): Twig\TemplateWrapper->render()
#12 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/Theme/ThemeManager.php(372): twig_render_template()
#13 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/Render/Renderer.php(433): Drupal\Core\Theme\ThemeManager->render()
#14 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/Render/Renderer.php(204): Drupal\Core\Render\Renderer->doRender()
#15 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php(238): Drupal\Core\Render\Renderer->render()
#16 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/Render/Renderer.php(580): Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}()
#17 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php(239): Drupal\Core\Render\Renderer->executeInRenderContext()
#18 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php(128): Drupal\Core\Render\MainContent\HtmlRenderer->prepare()
#19 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/EventSubscriber/MainContentViewSubscriber.php(90): Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse()
#20 [internal function]: Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray()
#21 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php(111): call_user_func()
#22 /home/keratai2/public_html/xxxxxxxx.com/vendor/symfony/http-kernel/HttpKernel.php(168): Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch()
#23 /home/keratai2/public_html/xxxxxxxx.com/vendor/symfony/http-kernel/HttpKernel.php(74): Symfony\Component\HttpKernel\HttpKernel->handleRaw()
#24 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/StackMiddleware/Session.php(58): Symfony\Component\HttpKernel\HttpKernel->handle()
#25 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/StackMiddleware/KernelPreHandle.php(48): Drupal\Core\StackMiddleware\Session->handle()
#26 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php(48): Drupal\Core\StackMiddleware\KernelPreHandle->handle()
#27 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php(51): Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle()
#28 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/StackMiddleware/StackedHttpKernel.php(51): Drupal\Core\StackMiddleware\NegotiationMiddleware->handle()
#29 /home/keratai2/public_html/xxxxxxxx.com/web/core/lib/Drupal/Core/DrupalKernel.php(681): Drupal\Core\StackMiddleware\StackedHttpKernel->handle()
#30 /home/keratai2/public_html/xxxxxxxx.com/web/index.php(19): Drupal\Core\DrupalKernel->handle()
#31 {main}
rgnyldz’s picture

My mistake, I did not run db update and cleared cache.

rgnyldz’s picture

I also have an edge case for you :)

I created a content type called Campaigns so the admins can create a page containing the promotion information and display it on a page with a reference field to promotions. So they have to only work on one page for all the info like promotion itself and images, descriptions etc.

But inside the node edit page the radio buttons inside the inline entity form(simple) do not work. I presume that the patch is specific for the promotion edit page ?

zaporylie’s picture

It seems to me like this patch won't work with coupons as the coupon validation constraint calls Promotion::applies() and that happens after the promotion preprocess removes all the promotion adjustments and before the processor adds them back.

Disclaimer - I didn't test that theory but working on something similar now so wanted to chime in.

bceyssens’s picture

patch #20 seems to work for us with latest 2.x dev version.

nicolas bouteille’s picture

Hello,

We have the same need than what was described on #9
For Black Friday, we want to offer -50% on all products, but also -70% on some categories.
Our clients need to be able to buy a product at -50% and another at -70% in the same cart / order.
It looks like as described in #9, in Commerce right now, when a promotion in incompatible with another one, it means they cannot be used in the same cart / order at all, even though they don't affect the same product. Which I think is too bad :/
I was actually surprised to discover this is the behavior so far.
Since Lisa's idea has been posted 6 years ago, then reposted on #9 4 years ago, I can only imagine this requires a lot of work and will not be available anytime soon… but can I suggest that this idea be at least mentioned in the description / roadmap of this issue?

Also, in the mean time, could you advise me on how you would implement my current need?
I was thinking about adding a custom plugin to be able to exclude specific products or taxonomies.
That way, I could create -50% on all products except one category, and another promo -70% on this specific category. Marking both promo compatible so that they can be both used at the same time but making sure on my end that they cannot affect one same product.

That makes me think that somebody has probably already created this kind of excluding plugin?

Thanks for your help.

Nicolas

nicolas bouteille’s picture

Ok just some follow up, here's how I managed to create a new condition to exclude some products (I will do the same for categories)
I just created a new plugin OrderItemExcludeProduct that extends OrderItemProduct and only overrides the evaluate() function to negate it:

public function evaluate(EntityInterface $entity) {
    return ! parent::evaluate($entity);
  }

I also took care of displaying a huge warning to make sure the person creating the promotion also adds another condition such as Specific product category AND selects 'Condition operator' : 'All conditions must pass'. If not, the consequence will be : the promotion will be applied on any product not excluded here, even if it does not match the other condition(s)...

By the way, the default value for Condition operator is 'Only one condition must pass' but shouldn't it say 'At least one condition must pass'?
I just tested this and it does not break if I have more than one condition passing...

derekw’s picture

Patch no longer applies to 3.0.2

flocondetoile’s picture

Version: 8.x-2.x-dev » 3.1.0

changed version

flocondetoile’s picture

Status: Needs work » Needs review
StatusFileSize
new11.39 KB

patch rerolled against 3.1.0 version

flocondetoile’s picture

StatusFileSize
new10.2 KB

For those who need the patch without the hook_update_N, as me who run this patch since 4 years (I implements the update_N in a custom module to not break other futur hook_update_N of the module), here it is.

PS: this patch is only for being added in some custom projects's composer.json

jsacksick’s picture

@flocondetoile: Can we get an MR? So we can see if the tests are passing/failing?

jsacksick’s picture

Version: 3.1.0 » 3.x-dev

I opened an MR with the patch from #31.

jsacksick’s picture

I'm curious, this patch was never RTBCED, @flocondetoile: Have you been encountering any issue in the projects where this has been applied?
I also think there are some use cases that won't work properly (due to how coupons are applied for example).

flocondetoile’s picture

I ran this patch since 4 years without issues. But I didn't have use case with coupons. I used it to make promotions incompatibles with others, without any coupons behavior associated with them.

derekw’s picture

I did have trouble using this patch when a coupon was applied and both were targeted at order items. When the coupon was applied no other discounts were applied even if they were applicable. Fortunately the coupon use was a rare case for us. Otherwise the patch worked.

Instead of using the Compatibility with other promotions feature I added custom conditions to evaluate order item product variation attributes for determining promotion applicability, and that was adequate to deconflict promotions.

chrisscrumping’s picture

We're using this and seems to be working fine

jsacksick’s picture

If I remember correctly we got this tested locally and multiple issues were found.
The issues are not strictly caused by the changes introduced but due to the current architecture / logic and the new strategies introduced suffer from the same limitations as the existing compatibility strategies.

This was the result of our own internal testing:

Found issues with "Compatibility with other promotions".

Tested separately with 3 "Compatibility with other promotions" settings; results for each test are shown in the table below.

Promotion Type Compatibility with other promotions Behavior
Test 5% Percentage off order subtotal – no coupons Not with any other promotions Applied both promotions – expected only Test 10% to be applied
Test 5% Percentage off order subtotal – no coupons Only with the selected promotions – selected promotion: Test 5% Applied both promotions – expected only Test 10% to be applied
Test 5% Percentage off order subtotal – no coupons Any promotion except the selected promotions – selected promotion: Test 10% Applied both promotions – expected only Test 10% to be applied
Test 10% Percentage off order subtotal – no coupons Any promotion —

This behavior occurs when, in the promotions list, the first promotion uses one of the restricting compatibility options and all other promotions are set to "Any promotion". For these restrictions to work, the promotion using the restricting option must have the largest weight, ensuring it is applied last.

There is no disclaimer text in the UI informing users about this – is this behavior expected, or is it a bug? And if more than one promotion used these limitation options, how would we order them / decide which weight each should have, since a promotion with a lower weight is not aware of the ones coming after it and does not validate against them?

Another issue: when selecting either "Only with the selected promotions" or "Any promotion except the selected promotions", the list of available promotions includes the promotion I am currently editing, which doesn't seem right.

Coupons

This presents a similar problem. If we have a promotion Test 10% that is compatible with any other promotion except Test 5%, and Test 5% is redeemable via coupon code, we are able to benefit from both promotions: Test 10% is applied automatically, and afterwards we add Test 5% with a coupon, which is accepted.

The other way around: Test 5% is not compatible with Test 10%. If we try to use a coupon for Test 5%, validation fails because Test 10% is already applied.

So the biggest issue with "Compatibility with other promotions" is that it checks what is currently applied to the order at that moment, and it checks promotions one by one – so depending on the weight of the promotion, the compatibility might or might not be respected.