Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Implement the commerce_cart_expiration (for Commerce 1.x) module solution into Commerce 2.x core
Bojanz suggestion:
it's basically an order type setting + a hook_cron() that does an entity query and adds order IDs to the queue, and a queue processor to delete the orders
Code History
Code history and changes can be found here: https://github.com/drupalcommerce/commerce/pull/639.
Comment | File | Size | Author |
---|---|---|---|
#79 | 2853527-79-cart-expiration.patch | 20.86 KB | bojanz |
#71 | 2853527-71.patch | 18.55 KB | flocondetoile |
| |||
#69 | interdiff-67-69.txt | 2.53 KB | flocondetoile |
#69 | 2853527-69.patch | 18.6 KB | flocondetoile |
| |||
#67 | 2853527-67.patch | 18.35 KB | flocondetoile |
Comments
Comment #2
rthornton CreditAttribution: rthornton at Acro Commerce commentedComment #3
rthornton CreditAttribution: rthornton at Acro Commerce commentedComment #4
drugan CreditAttribution: drugan as a volunteer commentedGood idea but it needs further work.
I have a cart 37 days old and assigned 2 days as cart expiration term but the cart was not deleted after running cron manually. Not explored this deeply but I think the reason is that the cart expiry setting is not actually saved (I expect to see 2 days here now) and might be not visible in modules/cart/src/Plugin/QueueWorker/CartExpire.php
Also, it would be useful to expire cart not only per day basis but also per hour too.
Comment #5
mglamanThere's been many adjustments, here is the PR: https://github.com/drupalcommerce/commerce/pull/639. I also have added a test.
Care to give the new patch a try https://patch-diff.githubusercontent.com/raw/drupalcommerce/commerce/pul...
Comment #6
drugan CreditAttribution: drugan as a volunteer commentedThe https://patch-diff.githubusercontent.com/raw/drupalcommerce/commerce/pul... still didn't work for me.
After applying interdiff-639.txt it works. Also, would be it more useful to set hours instead of days for cart to expire. Please, consider interdiff-639-hours.txt because it allows to set more precise expiry setting.
What I've found additionally is that if the cart was viewed on the admin/commerce/orders/{commerce_order} or admin/commerce/orders/{commerce_order}/edit or admin/commerce/orders/carts and the interval between the last cart refresh and current time is more than order type refresh frequency setting then the cart will be refreshed and therefore not removed. The same had happened when trying to get $order->cart->value before getting $order->getChangedTime() in modules/cart/src/Plugin/QueueWorker/CartExpire.php. Actually it had blocked any cart removing.
We need to keep this mind because there might come questions like: Why my carts are not removed in the time specified? Do not touch them and they will be removed.
Comment #7
shabana.navas CreditAttribution: shabana.navas at Acro Commerce commentedComment #8
mglamanThat PR should get added as a patch here and rerolled. We also now have
\Drupal\commerce\Interval
that could be used to help determine the run cycle.Issue needs triage in general.
Comment #9
flocondetoileWorking on this
Comment #10
flocondetoileHere the patch rerolled and improved :
- Use
Drupal\Commerce\Interval
- We can select now an unit for the expiration time
- #6 is fixed. This was because using orderStorage->loadMultiple() trigger the OrderRefresh and then change the order changed time to now.
Comment #11
mglamanAwesome!! Quick review notes. Will test locally as well.
Should we do a
!empty()
on checking for enable as well?It defaults to
'now'
. But maybe we can keep to be explicitGood catch!
Comment #12
flocondetoile1. Why not. But as the enable option is 0 or 1, this condition is sufficient. I wanted to be more "explicit" with other parameters.
2. To be honest, I'm not really comfortable when working on date :-). My first idea was to provide a DrupalDatetime with a DateTimeZone based on UTC (
new DrupalDateTime('now', new DatetimeZone('UTC'))
). But instead, for simplicity, I removed the DateTimeZone('UTC'), and then we use the default system timezone. And I let the'now'
parameter to stay explicit. But I have some doubt if we have to use (or not) the UTC TimeZone.Comment #13
mglamanCool, I'm fine keeping the
'now'
, just update to empty check, just in case.Comment #14
mglamanTo address #6
I do not know how often store managers go to view a cart or edit a cart. If they do so, I think that is valid that it persists longer (resets the countdown to deletion.)
This was fixed by using
loadUnchanged
.Carts may not expire, however, if the refresh was changed to
REFRESH_ALWAYS
. Anytime admin/commerce/order/carts is loaded the first page will have their refresh run. But, these are edge cases I can live with. Orders should only refresh when the current user is the owner of the order while in a draft state.See \Drupal\commerce_order\OrderRefresh::shouldRefresh.
I'm testing this manually locally.
Comment #15
flocondetoile$order_type->getThirdPartySetting()
method (in hook_cron and CartExpire), otherwise thenew Interval
will throw an exception if the order type has not been saved to set this new third party setting.Comment #16
mglamanWoo! It worked. Putting to RTBC for bojanz eyes.
Comment #20
mglamanSo, DrupalCI had some issues. Tests are fine. Back to RTBC.
Comment #21
carstenG CreditAttribution: carstenG at FFW commentedrerolled patch.
Comment #22
mglamanHas anyone been using this on their site to provide feedback?
Comment #23
mglamanThis should be changed to be a boolean, I feel like.
Wondering if this should be something different
EDIT: that's what we call it in Interval.
Comment #24
mglamanComment #25
mglamanHere is an updated patch.
enable
is now boolean in the schema and updated the test.Comment #26
mglaman"commerce_cart_expiration"
Rework, lower case C.
There's other fixes to go as well.
Comment #27
mglamanNew patch. The following remains.
Per recurring, the interval UX is to have number first and then the unit. And number should just be "interval"
See https://cgit.drupalcode.org/commerce_recurring/tree/src/Plugin/Commerce/...
Fix wording
Remove enable. If it's enabled we save a valid number value. If it is not enabled, we save
0
as the number value.Comment #28
flocondetoileJust deploy patch #27 on production. Everything is working fine. I will report in a few day once the big queue of abandoned carts will be purged.
Comment #29
mglamanFixes the UI items and removes
enabled
.Comment #30
bojanz CreditAttribution: bojanz at Centarro commentedWhy don't we provide a reasonable default, such as 30 days?
Why would we allow a minimum of zero? Shouldn't #min be 1?
Missing t().
Wouldn't it be shorter to provide no default and then do if (!empty($cart_expiration))
Same with the default in CartExpiration.
Let's use "<="
Comment #31
mglamanWhat if someone wants to disable cart expiration? By removing
enabled
we would always have it be configured.Makes sense -- we can save it as
0
if disabled and if empty/0 show 30.Yeah.
----
Also, I wonder if this should exclude orders attached to users. Right now it purges anonymous and authenticated carts. If we turn it on by default, then I definitely think we should. It's probably a good idea anyways to preserve authenticated user carts. Amazon does.
Comment #32
flocondetoileAnd what about a checkbox "Delete only anonymous cart" checked by default ?
Comment #33
mglamanActually, I don't know if we need to worry about authenticated users or not. If the expiration threshold is high enough, their cart will be refreshed whenever they come back (a day, a week, etc) later. This prevents it from being purged.
Comment #34
mglamanHere is an updated patch per bojanz review. Didn't concern about authenticated users, given thoughts in #33
Comment #35
mglamanWhile we're at it: this isn't a task, it's a feature request.
Comment #36
mglamanClean up default values in the form.
Comment #37
bojanz CreditAttribution: bojanz at Centarro commentedThis shows clearly that the form structure is odd, cart_expiration_interval instead of cart_expiration, and the prefixes on unit and number.
The file for CartExpirationTest is called CartExpirationsTest.
I'll clean that up on commit.
Comment #38
bojanz CreditAttribution: bojanz at Centarro commentedCartExpirationTest doesn't have test coverage for:
1) A deleted order doesn't prevent others from being deleted
2) Cart expiration can be disabled, and the queue worker will respect that and skip deletion
3 Carts that no longer qualify for deletion are skipped.
So no commit tonight. Attaching a patch with the #37 fixes and other nits.
Ryan suggested that we need a more direct phrase for the checkbox. Attaching a screenshot of the current version. Suggestions welcome.
Comment #39
bojanz CreditAttribution: bojanz at Centarro commentedUnassigning.
Comment #40
John Pitcairn CreditAttribution: John Pitcairn commentedI'd say just "Delete abandoned carts" for the checkbox, use form #state to reveal the interval field if checked, and label that "Delete after" or such.
Comment #41
mglamanWorking on test coverage
Comment #42
mglamanHere are the updated tests. Now we just need to bikeshed the label.
> Delete abandoned carts
Sounds great to me.
Comment #43
bojanz CreditAttribution: bojanz at Centarro commentedAgreed
Comment #44
mglamanChanged to Delete abandoned carts.
Comment #45
bojanz CreditAttribution: bojanz at Centarro commented@mglaman
The tests in #38 and #44 appear to be identical, don't see any new test coverage.
Comment #46
mglaman😭😭😭 I realized my work was behind yours, so I did a branch and -3way merge of the patch and apparently lost my tests. I'll add them back in tomorrow.
Comment #47
tuutti CreditAttribution: tuutti as a volunteer and at Druid commentedLooks like there's a trait that contains indentical
installCommerceCart()
function.Is this still missing tests?
Comment #48
jacobbell84 CreditAttribution: jacobbell84 commentedIt looks like 8.x-2.10 and above breaks this patch. I did some digging when I couldn't get it to work and it appears to be caused by the implementation from this issue (https://www.drupal.org/project/commerce/issues/2499645). Since the cron job is running the task under the anonymous account the QueryAccessSubscriber is trying to lock it down to only its carts, instead of allowing the entire order system to be queried.
It doesn't seem like there's any way to differentiate the cron job from a standard anonymous user, so the only fix I can think of is to disable the access checks for the initial abandoned carts entity query.
Comment #50
geek-merlinI guess the failing test also needs accessCheck like
Comment #51
flocondetoilePatch doesn't apply anymore on 2.12
Comment #52
flocondetoileSimple reroll from #48 with the missing ->accessCheck(FALSE) added on the test's queries.
Still missing tests #38 about
1) A deleted order doesn't prevent others from being deleted
2) Cart expiration can be disabled, and the queue worker will respect that and skip deletion
3 Carts that no longer qualify for deletion are skipped.
Working on this.
Comment #53
flocondetoileComment #55
flocondetoileMissing tests mentionned in #38 added
1) A deleted order doesn't prevent others from being deleted
2) Cart expiration can be disabled, and the queue worker will respect that and skip deletion
3 Carts that no longer qualify for deletion are skipped.
Comment #58
flocondetoileCart expiration was not well disabled for the fail.
Comment #59
flocondetoileFixing the test (I have recreated the cart1 instead of the cart2 which was previously deleted) for the latest test : A deleted order doesn't prevent others from being deleted.
But this shouldn't pass. I guess we may need a Try / Catch in the QueueWorker.
Comment #61
flocondetoileLooks like this is unrelated fails (because we test now again Core 8.7 ?). Switching to need review.
Comment #62
mglaman8.7.x broke a lot of kernel tests.
Comment #63
drugan CreditAttribution: drugan as a volunteer commentedI've tested the #59 on my local install with 8.6 and the CartExpirationTest is green.
Comment #64
flocondetoileOnce tests fixed against 8.7 #3038493: Tests are failing on Drupal 8.7, I will relaunch it.
Comment #66
flocondetoileTest relaunched but it failed because certainly of a missing $this->installEntitySchema() for an entity type (in reference to #3038493: Tests are failing on Drupal 8.7).
Comment #67
flocondetoileSame patch than #59. Local tests are green. To see what the bot says about it.
Comment #69
flocondetoileComment #70
flocondetoileMissing test were added. And tests are again green :-). Looks like RTBC ?
Comment #71
flocondetoilereroll against 2.13
Comment #72
smccabe CreditAttribution: smccabe as a volunteer and at Acro Commerce commentedSome small nitpicks, I think this patch is essentially RTBC, depending what is though of these small questions. Functionality worked as expected, tests look thorough and UI looked correct. I think this qualifies for bojanz review.
If changed doesn't work on save, why are we still passing it into the create/save code above?
Extra blank line
Should we also name all 5 test carts and test more explicitly for the ids that should or shouldn't exist, instead of just checking the number of remaining carts? I don't see any flaws currently, but it seems like being explicit might make it a little more durable in the future.
Comment #73
alexandersluiter CreditAttribution: alexandersluiter commentedWhat if instead of only expiry, it was a way to react on stale carts and allow custom actions via a triggered event and an event subscriber on the other end of it.
There could be:
This could serve multiple purposes. It would clean up old/stale carts as is the point of this issue. It would also serve and as a loose API for developers to fire off marketing related events based on the age of a cart or order.
From a UI standpoint, I'd imagine the "deleter" would be a simple checkbox and number input. Underneath, it would be a simple event trigger and subscriber.
--checkbox-- Delete old carts after --number-input-- minutes/hours/day
Then another form below this that would be an interval entity of sorts where the user could add "events" after the age of the cart is x minutes/hours/days old. The number of events should start empty, yet be unlimited and user configurable. There would be no default subscribers for these events. Developers could hook into them however they want. There should be some sort of "identifier" to an event that is configurable for developers to understand which exact event they are responding to.
Thoughts?
Comment #74
mglamanThe comments in #73 carry over from some Slack discussions.
From a rough viewpoint, we would need a "Nudge" and "Expire" event. The config schema then becomes a sequence of items that contain an interval and type of event through. Or it's a single event with an "action" string and that string is what is configured in the UI "nudge" or "expire", naming to be bikeshed.
By default, we'd ship with the expiry solution to react on expiration settings. It does blow the scope of the issue and adds complexity. But it allows us to nail expired carts and a simple API for remarketing tools without additional hacks or fear of conflict.
I haven't had time to sit and fully think on the consequences of this. On the surface, it seems "🙌woo 🎉 cart abandonment emails and cleanup?!" but I'm not sure what dragons await us.
Comment #75
freelockOoh this sounds cool...
With a little work, the Scheduled Message module should be able to plug in here for the notification part, if this is implemented as different states of the cart. That module is set up to use a state from State Machine (haven't yet converted to core Workflow), and a date field of some kind, and lets you trigger a message based on an offset from that date along with the state -- if the state changes, message gets canceled instead of sent (e.g. if the cart converts to an order).
Comment #76
jibranI don't think the order should be deleted. The order status should be changed to aborted/timed-out and it should be kept for reporting purposes. Or at least it should be configurable.
Comment #77
TwiiK CreditAttribution: TwiiK at Ny Media AS commentedIf it's configurable then I have no issue, but we are talking about deleting old cart orders, right? I can't think of many uses for old cart orders in reports. Our needs for something like this is to lessen our databasen size so disabling the orders wouldn't help us. We have a Drupal 7 site that we're porting to Drupal 8 soon with millions of orders, many of which are cart orders. Lately they've been complaining about the order admin being unusably slow and it's surely due to the sheer amount of records in the database. We have to solve this when moving to Drupal 8 and deleting all old cart orders is a step in the right direction. We only ever allow 1 cart order (the most recent one) per user anyway so these old cart orders do nothing except clog up the database slowing down the site.
I guess my point is that I feel deletion is the logical default here, but it could by all means be configurable out of the box if people see a need to keep old cart orders.
Comment #78
bojanz CreditAttribution: bojanz at Centarro commentedSince there's been a lot of discussion recently, as well as talks of introducing an event, I am bumping this issue back to "needs review", to indicate that it's not simply waiting for someone to hit the commit button.
Comment #79
bojanz CreditAttribution: bojanz at Centarro commentedI have gone through this issue again, reviewed the current code and all comments.
I recognize that sites have different needs.
a) Email the customer after X days
b) Delete or mark as expired the order after Y days
However, a) and b) are not the same feature. Sites that email customers will still want to clear their old orders if they fail to respond within a given time period. So let's leave emails to Commerce Abandoned Carts.
It is tempting to try and make this issue solve all use cases, but that opens additional UI questions. Meanwhile, our requirements are simple and repeating across projects: Delete old carts so that they don't clog the database. That's why I've decided to proceed with this issue as-is. This feature is off by default. Sites that wish to retain carts and change their state are free to write custom code for that (using this feature as a template), and keep the deletion disabled. Sites that wish to send the cart details to an external system before deletion can respond to OrderEvents::ORDER_PREDELETE (or hook_entity_predelete).
Attaching a rerolled, cleaned up patch. Moved the commerce_cron_logic to a commerce_cart.cron service, to allow it to be swapped out if needed.
Comment #81
bojanz CreditAttribution: bojanz at Centarro commentedCommitted!
Big thank you to rthornton for getting this started, to flocondetoile and mglaman for doing most of the work needed to complete the work, and everyone else who pitched in with improvements, rerolls, testing.
Comment #82
jsacksick CreditAttribution: jsacksick at Centarro commented@bojanz:
Why limiting to 50?
Comment #84
bojanz CreditAttribution: bojanz at Centarro commentedYou're right, it's too conservative. Raised it so that we get up to 250 ids, chunk them into groups of 50, then queue them for processing. Let's see how that treats us.
EDIT: Pushed another commit to add the commerce_cart_expiration tag to the entity query, so people can use hook_query_TAG_alter() to lower/increase the number of processed items.
Comment #86
jacobbell84 CreditAttribution: jacobbell84 at ZenSource commented