Hi

Talking with bojanz and iMiku this morning we discussed a method for expiring a cart under X conditions, which would be to provide a rule condition that could be configurable to set what conditions expire a cart, session expiration, 3 months of inactivity, etc, and in the rule set, provide an action which would be entity delete (order).

Not sure if we can trigger a rule on "cron run" event.

Would this be commerce core material or contrib?

CommentFileSizeAuthor
#9 1272474-cart_expire_rule-9.patch2.36 KBpcambra
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

dernetzjaeger’s picture

That is a feature I am looking for!

I think it belongs to the core.

regards, Marc

bradhawkins’s picture

Subscribe.

lukus’s picture

@bradhawkins; you can click follow now.. check out the button up there ^^ .. I only realised a few days ago.

bradhawkins’s picture

Thanks! : )

johnodonnell@mac.com’s picture

Just checked rules and "Cron maintenance tasks are preformed" is available as an event. So that's a start.

Order's have a status field that can be set to "canceled", which apparently makes it impossible to complete an order. There's already an action for changing a Status. That might be an alternative to deleting an order. Also, there's an existing action named "Remove all products from an order".

So it looks like we need to loop through all orders and
* check the status (cart, checkout, pending or completed)
* check the last time it was Updated (or Created if you want to be more strict)
Whatever fails those two tests are either deleted or the status is set to "canceled".

I think the big hurdle will be the condition part of the rule. We need to loop through all orders, test each one and only take action when there's a match. I don't see a way to do that in rules.

The only way I can see of doing this is to write an action that does the looping for you, with a config form to tell the action that status and age at which to expire an order.

Any thoughts? I might need to write something similar to this real soon.

pcambra’s picture

Thanks for the details!

I'd add that we need to define a set of conditions so not to loop all orders but those that match.
- Order state/status (provided by commerce)
- Expire time => order update timestamp checked with a fixed value in months, weeks, days... (data comparison)
- User session expiration, this doesn't belong to this event.

Once the conditions are passed, rules has a way to loop over the resulting entities and we can delete the order OR mark it as cancelled.

I'd like to know Ryan's thoughts on this one.

pcambra’s picture

Assigned: Unassigned » pcambra

Ok, first try to implement this drove me to a dead end, we can't fetch entities by properties in rules for state as it is a "virtual" property that doesn't exist in database.

Opened two issues for this.

- One in Entity API to be able to provide a callback for what it's called computed properties: #1345882: Undeprecate 'query callback' when used with a 'computed' property
- Once this is applied, we can fix this other one I've opened in commerce, for changing all the properties not belonging to database to computed properties and provide a callback #1345890: Allow to query order by states from Rules

pcambra’s picture

Status: Active » Needs review

We still need those patches above to correctly react only in orders by state, but I've modified my rules to be based in cart status (if some module added new statuses to Cart state, this rule won't use those, that's why we need to use state).

Here's the rule exported, is splitted in two as we need to be able to apply new conditions after fetching all orders in cart state/status, so here's a rule component that deletes orders passed by parameter with configurable conditions. By default uses current date - 15 days to delete orders older than 15 days. We may want to be more relaxed by default, 1 month-3 months?

{ "rules_commerce_cart_expire" : {
    "LABEL" : "Commerce cart expire",
    "PLUGIN" : "rule",
    "TAGS" : [ "commerce" ],
    "REQUIRES" : [ "rules" ],
    "USES VARIABLES" : { "commerce_order" : { "label" : "Commerce Order", "type" : "commerce_order" } },
    "IF" : [
      { "data_is" : {
          "data" : [ "commerce-order:changed" ],
          "op" : "\u003c",
          "value" : { "select" : "site:current-date", "date_offset" : { "value" : -1296000 } }
        }
      }
    ],
    "DO" : [ { "entity_delete" : { "data" : [ "commerce-order" ] } } ]
  }
}

And here's a rule that loops the order in cart status and uses the component above to delete them.

{ "rules_cart_expiration" : {
    "LABEL" : "Cart Expiration",
    "PLUGIN" : "reaction rule",
    "TAGS" : [ "commerce" ],
    "REQUIRES" : [ "rules" ],
    "ON" : [ "cron" ],
    "DO" : [
      { "entity_query" : {
          "USING" : { "type" : "commerce_order", "property" : "status", "value" : "cart" },
          "PROVIDE" : { "entity_fetched" : { "commerce_order_fetched" : "Commerce order fetched" } }
        }
      },
      { "LOOP" : {
          "USING" : { "list" : [ "commerce-order-fetched" ] },
          "ITEM" : { "order_item" : "Current order" },
          "DO" : [
            { "component_rules_commerce_cart_expire" : { "commerce_order" : [ "order-item" ] } }
          ]
        }
      }
    ]
  }
}

Rules forces you to set an amount of entities retrieved by entity fetch by property, but I think that's ok and I've set it to 10.

I'd appreciate feedback on this.

Once the patches in entity and commerce get in, I'll roll here a proper patch.

pcambra’s picture

And here it is, a patch that includes a disabled default rule to expire carts.

We may want to revisit this if something gets out of #1382022: Allow components as conditions

bdsl’s picture

Is it possible for someone to export this rule, so it can be imported into the database and we can avoid patching existing installs of Commerce? At least until this gets incorporated into a release?

rszrama’s picture

Component: Cart » Contributed modules
Status: Needs review » Postponed

Ahh, you know, this might not actually get incorporated into a release. I realized during a training a couple weeks ago that this just may not be a great match for Rules based functionality. The problem is that Rules doesn't give you a flexible enough query builder to properly make use of SQL so this doesn't become a performance hog... realistically you'd want to load the ID of any order in a cart status whose last update was X days / weeks ago in a single query. You don't want to have to load each potential match based on status and loop over them all checking their timestamp each time.

I think it's fine that Rules doesn't provide a more feature rich query builder, fwiw - some things just take code, and imo, this is one of those things. It may be this just needs to become a contrib that simply implements the cron hook, only loads the orders that actually expired, and passes them through an expiration Rules event. The precise time of expired carts can just be configured through a settings form I'm guessing.

pcambra’s picture

Fair enough, let's make a contrib then with a EFQ or a regular query (a really small contrib I'd say).

Also if we use a the hook_cron from the contrib we let room for elysia cron or similars to handle the execution for more performance control.

rszrama’s picture

Yeah, having it separate from the normal cron process would be excellent, essential even for large volume sites. E-mail is never easy. : D

pcambra’s picture

So I'm providing a little feedback here as I know amateescu is already working in this module :)

From my perspective we need just three elements here for building this:

- A very own hook_cron invoking a rules new event so it can be controled by cron-performance modules.
- A rules condition that gathers the orders in state cart with EFQ or a regular query and accepts a time constraint as parameter.
- A default rule using the cron event defined by the module and the condition for getting the orders and deletes them.

bdsl’s picture

The reason I was looking for this sort of function is because I had a requirement to support closing and opening the store. In case anyone else is looking for something similar, when the administrator sets the store to closed, I call this function:

function _cancel_shopping_carts(){
  $result = db_query("update commerce_order set status = 'cancelled' where status='cart';");
  drupal_set_message("All current shopping carts cancelled");
}

I also have a rule to stop customers adding anything to their carts when the store is closed.

amateescu’s picture

Status: Postponed » Needs review

Finally had time today to re-write my initial code based on Pedro's comment from #14 and publish it: http://drupal.org/sandbox/amateescu/1454320

I'll keep this in needs review for a few days then I'll promote it to a full project. Big thanks to @pcambra for allowing me to build up Rules skills with this one :)

pcambra’s picture

yay!

MickL’s picture

is it possible to manually delete all "orders" with the status "shopping cart" at one stroke ?

amateescu’s picture

Yep, that should be possible with http://drupal.org/project/commerce_vbo_views

rszrama’s picture

Status: Needs review » Fixed

Let's go ahead and mark the issue here fixed. Feel free to promote it at any time, but you'll need to update your action. It's still based on just expiring orders in the actual "cart" status, but remember there are more than one shopping cart statuses that should be expired. Here's the snippet from commerce_cart_order_is_cart():

  // If the order is in a shopping cart order status, assume it is a cart.
  $is_cart = in_array($order->status, array_keys(commerce_order_statuses(array('cart' => TRUE))));

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

vlyalko’s picture

Excellent module to address the cart expiation issue is: http://drupal.org/project/commerce_cart_expiration