Problem / Motivation
My goal is to allow a customer to apply a coupon and browse the catalog and see the live discounts being applied. At the moment a customer may only see the discount when the product is in the cart.
The commerce_coupon_commerce_cart_order_refresh() function is currently removing all instances of discount_coupon that do not have the referenced discount on the order through the commerce_coupon_remove_coupon_from_order() function.
A discount rule already has a condition of "Coupon code for a particular discount has been added to an order" which makes sense. However the logic in commerce_coupon_commerce_cart_order_refresh is basically saying that a discount rule evaluating to true is a condition for the coupon. That feels like circular logic.
Proposed Solution
My initial thought would be to remove commerce_coupon_remove_coupon_from_order() from commerce_coupon_commerce_cart_order_refresh(). A coupon already has its own conditions (usage, dates, etc.) to evaluate and if those conditions pass then the coupon is valid and can be added to the order.
Scenarios
1. If a coupon is provided and the coupon conditions FAIL then the coupon is NOT added to the order. The discount rule will always fail because the coupon is not on the order.
2. If a coupon is provided and the coupon conditions PASS then the coupon is added the order.
2a. If the user browses the catalog the discount rule is evaluated on each product display. If the discount rule conditions PASS then the product display shows the active discount.
2b. If the user browses the catalog the discount rule is evaluated on each product display. If the discount rule conditions FAIL then the product display does NOT show the discount.
2c. If the user adds a product to the cart and a discount rule conditions PASS then the discount is added to the cart. The user can see the discounts applied in the cart.
2d. If the user adds a product to the cart and NO discount rule conditions pass then NO discounts are added to the cart. The user does NOT see any discounts.
Caveats
The Order coupon list View that shows any coupon on the order could potentially show coupons that had no effect on product pricing in the case of (2d).
I was speaking with mglaman on IRC earlier and he had the suggestion to make it an option on the coupon itself. e.g. "Allow coupons to be added without discount applied"
I like this idea which essentially makes the coupon removal optional... however it doesn't negate (2d) from happening.
| Comment | File | Size | Author |
|---|---|---|---|
| #43 | allow_discount_coupon-2263315-43.patch | 2.73 KB | 2ndmile |
| #36 | allow_discount_coupon-2263315-36.patch | 2.73 KB | mglaman |
| #25 | commerce_coupon-apply_without_discount-2263315-25.patch | 2.68 KB | msupko |
Comments
Comment #1
mglamanI think this is a use case feature, however, I could also see it as a re-used feature if it was available. I have come across some extreme discount and promotion proposals. I think this extends the flexibility of the module without harming its core focus. After all, a coupon merely grants you access to something - why can it not simply grant you access to viewing discounts?
The concern I've had is that commerce_coupon_remove_coupon_from_order() is used to do garbage collection. We need to do this in Commerce because items can be added or removed from the cart and nullify the original conditions which allowed them to be placed. The order does not get refreshed once it leaves 'cart' state, unless it re-enters via canceling checkout.
A consideration I had is moving garbage collection to occur when use count is incremented, prior to checkout being completed. The end goal is for a end order to not have any cruft attached or skew coupon/discount usage. I'm suggesting this but feel its an improper approach and half endorse it. The context seems proper on an order when it has the 'cart' state, not before completing checkout.
Adding the option "Allow coupons to be added without discount applied" can allow bypass of the order refresh clean up. I'm just wondering the best steps to provide garbage collection if that option is enabled.
--------
Two cases instantly came to mind while writing this that past clients would have used this for:
Comment #2
caschbre commentedI can definitely see wanting to do garbage collection on the order and agree it should probably happening when entering checkout as opposed to during cart price calculation.
Could we do the garbage collection at the point the user submits the cart? Or in cases where a site doesn't use the cart, when the checkout process begins?
----
Also... just to add to the conversation... the use case I'm needing this for is allowing a coupon code to be passed via querystring. In a custom module hook_init I look for the querystring parameter and call the coupon redeem function. The client wants to be able to have promos running from banner ads, give special URLs out at conventions, etc.
Comment #3
mglamanAfter discussion in IRC, caschbre brought up how this option could be stored and modified. We discussed having it be a setting on each coupon, providing a more robust control over how coupons operate (not all or nothing.) That way you can have special coupons but still normal coupons.
Saving this value could be simply tucking it into the entity's data attribute and pulling it out of the array when needed, much like Commerce Shipping does for its own line items.
Comment #4
caschbre commentedAfter more IRC with mglaman we think the approach would be something along the lines of...
1) Add the "Allow coupon to be added without discount applied" to the discount_coupon. Save this in the data column.
2) Check this new option prior to calling commerce_coupon_remove_coupon_from_order() in the commerce_coupon_commerce_cart_order_refresh() function.
3) Add similar logic for garbage collection to commerce_coupon_commerce_checkout_form_review_validate().
Anything I'm missing?
Comment #5
caschbre commented@mglaman...
With the way the coupon form is implemented I'm struggling to figure out how to take the value of the new field and save it to the 'data' column of the coupon. Any pointers?
Attached is a patch that does two things:
* Comments out the function call that removes coupons from the order without the associated discount.
* Adds a new field to the coupon form in commerce_coupon_attach_coupon_entity_form().
Comment #6
mglaman@caschbre, check this out. commerce_coupon_form_submit() is the callback for the entity form, added it there. also made the order fresh conditional on coupon's setting. Not tested because I quick proof of concepted it to help bring this closer to solution, won't have much time tonight.
My IDE strips whitespace..so sorry about patch.
admin.inc, see addition of ->data below ->status
.module (refresh improved)
.module (your form area, improved a bit)
Comment #7
caschbre commentedHere's an updated patch that...
1) Optionally lets a user allow a coupon to be applied to an order without the discount being applied. This is useful for showing coupons while browsing the catalog.
2) Updates the refresh function to use that option.
3) Adds the garbage collection to commerce_coupon_commerce_checkout_form_review_validate().
Comment #8
mglamanThis is an existing hook, but my only question is: is that clean up logic copy & pasted from order refresh logic? Should this patch move the logic into a helper function and then call the helper function at both locations? That could make the process more manageable and future proof on any changes in logic.
Comment #9
caschbre commentedIt's somewhat copy/pasted. I stripped out the condition that checks data['apply_without_discount'] and the $error logic. So all that's left (copied) is the loop, an IF statement, and the commerce_coupon_remove_coupon_from_order() call.
There's more to each function but here's the relevant snippet from each.
commerce_coupon_commerce_cart_order_refresh
commerce_coupon_commerce_checkout_form_review_validate
Comment #10
mglamanWell, I think I'm just splitting hairs then :)
I'll test sometime this week on my local dev
Comment #11
caschbre commentedFixed a bug in the last patch. A missing ]. Ugh.
Comment #12
caschbre commentedFor some reason #11 wasn't applying cleanly due to the whitespace stuff. Here's another reroll that seems to apply cleanly.
Comment #13
mglaman$entity->data actually comes as a serialized array. Value never gets set properly on entity. I'm having some issues getting it to work properly.
Finals for this class wrap up today, I'll test more tonight or tomorrow.
Comment #14
caschbre commentedYeah, I was noticing it wasn't saving the other data and haven't had time to debug it yet.
Comment #15
mglamanThe real problem here is that the entity controller doesn't automatically unserialize the data property for us, like line items do. Opened issue here: #2280505: Support automatic entity data unserialization
If we can get a patch written for that it'd make this process much simpler to manage.
Comment #16
caschbre commentedThe patch from #2280505: Support automatic entity data unserialization seems to have done the trick and patch #12 works for me so far.
Comment #17
caschbre commentedHere's an update to #12. I noticed I had used $commerce->data instead of $coupon->data in one of the statements.
Comment #18
caschbre commentedFor some reason #17 wasn't applying right. So I recreated it without all of the white space mess.
Comment #19
caschbre commentedUgh... and in remaking it I had left out the part from #9. Ok, let's try this again.
Comment #20
caschbre commentedUpdated patch with a minor but important fix.
Changed
'#default_value' => isset($coupon->data['apply_without_discount']) ? $coupon->$data['apply_without_discount'] : 0,to
'#default_value' => isset($coupon->data['apply_without_discount']) ? $coupon->data['apply_without_discount'] : 0,Comment #21
caschbre commentedRe-rolled. Note that we've been using the previous patch for the past year in production.
Comment #22
torgospizzaThanks for working on this @caschbre! One of the best features when we were using Ubercart on D6 was the ability to include a coupon code in our marketing emails, so that a user could hit a catalog URL (like e.g., rifftrax.com/catalog?coupon=PROMO1) and the coupon would be attached to their session, but of course the discount would only apply to a product once/if it was added to their cart. It seems like this issue gets us closer to having this same functionality in Commerce, so I am looking forward to testing it out.
Comment #23
caschbre commentedThanks @torgosPizza! That's exactly our use-case... we have users coming to a page with a coupon in the url that we want to track regardless of the discount actually getting applied.
Make sure you apply #2280505: Support automatic entity data unserialization otherwise this patch will probably cause an issue.
Comment #24
msupko commentedThe client @caschbre and I are working for identified an issue with this patch...the "Apply Without Discount" property saves correctly if an admin is adding/editing a coupon on the coupon form, but it does not save if the coupon is being edited inline elsewhere, for example, while adding/editing a discount.
This updated patch relocates the logic for saving this property from
commerce_coupon_form_submit(), which is only called when editing a coupon directly, tocommerce_coupon_form_attach_coupons_validate(), which is invoked on any occasion.Comment #25
msupko commentedAfter further testing, we discovered that
commerce_coupon_form_attach_coupons_validate()isn't actually invoked when saving a coupon directly from the coupon form. Attached is a patch that combines the best of #21 and #24 to cover all cases.Comment #26
torgospizzaPatch in #25 seems to work great. I'm looking forward to using it in production.
Comment #27
torgospizzaTesting further, I'm wondering if we should introduce an option that allows a coupon to be stripped from the order at checkout if no discounts apply? It seems kind of weird to have a coupon in the Coupons View/list but have the Value be blank.
It's definitely a nice feature to be able to apply the coupon in this way (especially with links in email blasts!) but I do think there should be some consideration into whether we want a no-discounts coupon to still be sticking around once the user has made it to Checkout.
EDIT: Upon even more testing, I think this patch has either created or revealed a problem with Discounts, which is that a Product Discount can be removed from an order after it has been calculated and added as price components. The coupon remains attached but the Discount reference gets removed from an Order, sometimes resulting in the erroneous removal of the coupon as well.
Twice now I've had to check the "apply without discounts" checkbox for coupons that used to work without issue. So again I'm not sure if this is a regression or in fact we are getting to the route of a larger issue with how Discounts alters line items and discount references when the discount is a product-level discount.
Comment #28
caschbre commentedIt could possibly be a configurable option for a coupon to be removed at a certain stage in the checkout process if the discount doesn't remain... but also remember that a coupon can trigger non price related actions. So depending on the use-case it does make sense to keep the coupon on the order even if a discount isn't applied.
Comment #29
torgospizzaYeah that makes sense. Also I may just not have understood how coupons work when they are part of a product discount. The order_refresh stage applies the discount as a price component but removes the reference to the discount itself, but keeps the coupon intact. Which I guess makes some sense :)
Anyways I am a fan of this change. I think we could address the concern of "orphaned coupons" later on.
Comment #30
mglamanPutting back to needs review because this always depended on a patch, so it couldn't be RTBC. BUT! The dependant patch has been committed #2280505: Support automatic entity data unserialization. Could those involved test against latest -dev?
Also. I've committed a base test class. A test would be even better. #2614392: Add tests! Start with basic UI tests..
Comment #31
torgospizzaHmm, something in the latest dev has definitely broke something. When saving a coupon with the "Apply without discounts" box checked, I'm receiving a warning: "Illegal string offset: apply_without_discounts in commerce_coupon_form_attach_coupons_validate() line 1819"
Which is this:
Not sure how to best resolve it. But the warning has the effect of leaving the "Apply without discount" value being unchanged.
EDIT to add: I can't reproduce this locally, of course. Argh. (Maybe because we're using Memcache/Entitycache on the live servers?)
Comment #32
torgospizzaLooks like setting the data array if one doesn't already exist is the solution:
Not sure if there is a more elegant way to do this but it appears to be necessary.
EDIT: Upon further inspection it seems that mainly doing this through the Discounts form (with the inline coupon form) also has some issues there. When doing it right through the coupon form (from the list of coupons) it also seems to work. Will try to investigate further, but for me I think it finally saved properly when I edited the coupon from the Coupon Edit form (and not the Discount edit form).
Comment #33
mglamanDoes this look to be an issue where the controller needs to ensure this is an array on load if empty? Would need to look again at other controllers. Or is it an implementation issue of the inline coupon and discount form?
Comment #34
torgospizza@mglaman: I'm not actually sure. Even with my fix, I'm now getting this warning:
Creating default object from empty value in commerce_coupon_form_attach_coupons_validate()The object in question is $coupon, so I'm not sure if that means the form is no longer passing the actual object in through its values or what. I'll try to keep digging if I can. Luckily, saving does seem to work through the inline form now!
Comment #35
mglamanMarking as needs work for tests,
also we should alter commerce_coupon_discount_coupon_codes_exist_on_order() as I think that'd allow product discounts to render properly.Comment #36
mglamanAdd an isset() check during refresh in case data attribute is missing.
Comment #37
torgospizzaSaw a patch but the status is the same. Setting to Needs Review.
Comment #38
torgospizzaIt would also be interesting to add a section that does this:
that way a user can just add ?coupon to the end of a URL to apply the coupon to their session. This is something currently you'd need to implement in a custom module.
Comment #39
2ndmile commented#36 Working for me so far.
Comment #40
mschudders commentedI am having issue where I am applying a correct coupon(product discount) to an order, but the conditions will fail because the wrong product is in the order.
Thus the coupon / discount is not applied > Correct !
But however I am seeing a message: "Your shopping cart has been updated."
This is not correct, because it has not been updated, because it is actually a wrong "discount code".
Does anyone else have this issue ?
Comment #41
torgospizza@mschudders: No, I haven't experienced that issue. Does this only occur when you have applied one of the patches from this issue? Can you verify that reverting the patch resolves the behavior you're seeing?
Comment #42
2ndmile commentedWell some strange things have started to happen. The serialized data being saved in the database is in a strange format so that when unserialized, it is not an array. Therefore,
$coupon->data['apply_without_discount']returns NULL.I believe because of this, the coupon is not being applied to the order.
Comment #43
2ndmile commentedUpdating patch to run against 7.x-2.0.
Also added
$coupon->data = array();to get data to be serialized properly.