Describe your bug or feature request.
Let's say 2 promotions are applicable on the order with total 100,-:
- 20,- off - activated on all eligible orders
- 30% off - activated via coupon
Promotion 20,- has a weight -1 (should apply first)
Promotion 30% has a weight 1 (should apply last)
The expected promotion application is as follows:
100 - 20,- = 80,-
80 - 30% = 56,-
order total is 56,-
Current behavior:
100 - 30% = 70,-
70 - 20,- = 50,-
This happens as promotions activated via coupons are not respecting the promotion weight and are always applied before regular promotions.
Comments
Comment #2
jsacksick commentedThere has been several issues opened related to this behavior. changing this now would be a BC. Applying coupon based promotions first was done by design afair, I don't completely recall the rationale behing this decision though.
Comment #3
zaporylieSurely we could do something to provide a backward-compatibility layer? For example
- Create a config that will determine global behavior for when coupon promotions should be applied - change it to TRUE via hook_update, default to FALSE for all new installations
- In order processor collect coupons, and associated promotions
- If the site opt-in for applying coupons first, apply them (same as it is today)
- If the site opt-out from applying coupons, merge them with available promotions and sort based by weight
- Apply all applicable promotions
Comment #4
zaporylieFor the context - this is coming as a request for one of our clients in fashion industry. According to them it's an industry standard to create a coupon-activated promotion that will discount products already affected by another promotion. So for anything that's already discounted they want to have a flash-sale 10% off (or similar) which will apply additional discount on top of already discounted prices. This can be done by setting weights accordingly, but not if promotion is activated via coupon as that voids the promotion weight given all coupon-promotions are applied first.
I am ok with proposing a patch but can't afford spending time on creating one if it will be rejected anyways given there are some reasons for this being handled as it is today.
#3010862-25: Add missing promotion compatibility strategies is related
Comment #5
introfini commentedThis is a common use case in which the store is on sale and a coupon is provided to be applied on top of that. However, because the coupon is always applied first, you unexpectedly provide a larger discount than expected.
The client is perplexed as to why this occurs. To make matters worse, we can't explain why it "has to be this way."
@zaporylie's proposal appears to be very feasible, don't you think, @jsacksick?
Comment #6
agoradesign commentedI have a different problem regarding the promotion weights of coupon and non-coupon promotions.
You cannot apply a coupon promotion that is not compatible with others, when a non-coupon order already applies to a certain cart - no matter which weights the promotions have.
A concrete example, we have:
It's no problem, when A and B get combined, also no problem B+C, but A+C should never be allowed. We do not care, if A is applied and B cannot be applied then anymore, but it should always be possible to enter the coupon code of A.
So, let's assume our cart's total is above 50,- - so B and C get applied automatically.
If we make A compatible with other promotions, we are allowed to enter the coupon code and A gets applied additionally -> but we do not want to allow A+C, so that's a problem.
If we declare A as non-compatible with others, we simply do not have any chance to get the coupon code applied, although it's prioritized first, but unfortunately the automatically added promotions are already applied to the cart, so A loses this battle and won't get applied at all
Comment #7
loze commentedIf coupon promotions are always applied first, why is there the option to set the weight in the first place?
Isn't the point of ordering the weight of the promotions to let the admin decide what they want? or am I missing the point?
Comment #8
jsacksick commentedI'm not against changing this... We'd probably need to expose this setting in a new UI though and tests coverage...
Also, as far as I remember, whenever I tried merging coupon based promotions with the ones returned by
loadAvailable(), tests were failing... I think the promotion compatibility doesn't work as expected in this case.I don't exactly remember, but there was some logic failing...
@zaporylie: Let me know if you're still interested to work on this.
Comment #9
zaporylieI can take a code out from the client's codebase where I worked on the workaround for the current limitation. I will post it as a patch in the following days. Glad to hear you're open to make this change.
Comment #10
zaporylieHi again, I thought I would have time to do this before my family vacation, but I didn't. I am still interested in providing a patch/MR, but that will realistically happen in more than 2 weeks, so if someone else is interested in working on this issue feel free to do so.
Comment #11
tbkot commentedComment #13
tbkot commented@jsacksick It can be tested.
I've added a new field for the promotion entities to control when it will be applied, before or after coupons.
I guess it should not be a breaking change and should give more flexibility, just in case.
We can add additional configuration to define the default value for a new field to affect all promotions if needed.
Comment #14
jsacksick commentedHm... I was thinking more of a global configuration / flag.
On new installs, promotions / coupons should be evaluated at the same time, and for existing installs, the behavior should remain the same.
Handling this as a setting that can be set from the settings.php isn't exactly ideal as it can be easily missed.
I don't think it makes sense for this flag to be per promotion, but open to discussing that during our next call.
Proposal for the setting:
Label: "Apply all promotions by sort order to enforce compatibility, including those with coupons."
Description: "If disabled, coupon based promotions will apply first regardless of compatibility rules."
Key: "enforce_weight_ordering"?
Config object:
The logic should look like the following:
I think the main problem with this change is the error messaging whenever a customer applies a coupon code that can't be applied due the promotion compatbility.
With this new global approach, one could still allow a couponless promotion to apply with a coupon based promotion thanks to the compatibility settings (If it's set to "Compatible with any promotion").
Fixing this should allow us to close #2869209: "Not with any other promotions" does not stop other promotions from applying as a duplicate and should also unblock #3010862: Add missing promotion compatibility strategies which currently doesn't work well due to this bug...
Let's see if with the suggested changes, the logic works properly.
Also, perhaps there is no need for an update hook? If the config object isn't set then the setting won't evaluate to TRUE?
Regarding the settings form location, after discussing this with @rszrama, we think it should be under Config -> Orders -> Promotion settings
Comment #15
tbkot commentedComment #16
jsacksick commented@tbkot: Thanks! Could you update the setting label + description to:
Label: "Apply all promotions by sort order to enforce compatibility, including those with coupons."
Description: "If disabled, coupon based promotions will apply first regardless of compatibility rules."
Also, the test should be updated to test that the compatibility logic works as expected as well.
For example, if a coupon less promotion applies before a coupon based promotion, but the coupon based promotion isn't compatible with other promotions, it shouldn't apply.
Can we test this use case in particular? I believe right now this test would be failing without the changes and this is the main thing we're trying to address. Ensuring the weight is respected is needed in order to fix the compatibility (which is the primary underlying issue we're fixing here).
I think the PromotionCompatibilityTest should be expanded instead.
Comment #17
tbkot commented@jsacksick, it should now consider promotions that are not compatible with the other promotions. However, it does not cover that many options as in the #3010862: Add missing promotion compatibility strategies
Comment #18
jsacksick commented@tbkot: I'm surprised getCompatiblePromotions() is needed? Promotion::applies() is supposed to check for compatibility already? If not then I guess we have to fix that? we're kind of duplicating the compatibility logic in both places.
Comment #19
jsacksick commentedI pushed some changes (See https://git.drupalcode.org/project/commerce/-/merge_requests/550/diffs?c...). But I think this is still not sufficient, fixing the compatibility logic from the promotion order processor isn't actually trivial.
The problem is, when
$promotion->apply()is called, there is no guarantee that the promotion applied:For example, if the promotion offer is an instanceof
OrderItemPromotionOfferInterface, its possible that the offer conditions don't actually evaluate... In which case the promotion didn't apply.But also, even for the other case:
It's actually possible that no adjustment was added, or in the case of the
BuyXGetYoffer, it's possible that no extra order item was added.I think what we need to do (perhaps performance consuming is check that there is either a "promotion" or a "shipping_promotion" adjustment added to the order. THis should confirm 100% that a promotion was applied.
Will work on adding that check.
Comment #20
jsacksick commentedAlso... one of the problems with this change is we're actually re-evaluating promotion conditions for coupon based promotions... Since this is currently already done when the coupons field is validated, via the following code:
This invokes the CouponValidConstraintValidator which does this:
Which is actually the reason why coupon based promotions were then simply applied by calling $coupon->getPromotion()->apply().
Comment #21
jsacksick commentedI pushed a commit to address #20. With that said, the tests are failing which leads me to believe our tests are incorrect.
Comment #22
jsacksick commentedComment #24
jsacksick commented