starmatt's avatar

e-Shop with carts on a timer

Hello everyone,

I am working on an e-shop with laravel 5.1. My issue is that I want to have the carts in my database deleted after a certain time if the order has not been processed.

I am currently trying to get my head around the scheduler and cron, what I want to do is basically this:

Item is added to cart -> if cart has not been processed in (let's say for now) 30 minutes -> delete carts from database. If new item is added to cart -> timer is reset -> cart will be deleted in 30 minutes if not processed.

So I want to make some kind of timer starting at timestamp of the last added item. What would be the easiest way to accomplish this ? I can't see how to make it work with a scheduler and my database timestamps...

Thank you all for your time and attention!

0 likes
14 replies
d3xt3r's avatar

You can fire an event whenever a product is added to cart, listen to it and schedule a clean up job. But obviously this is not an efficient solution.

So an advice will be to run timely cleanup jobs and if you are to be so strict about the last added timestamp, when the cart is being accessed , you can check if it has expired or not.

bobbybouwmann's avatar

You can set a timestamp in that database for the last added item. At that moment you check if the schedular if difference between the time of the cart and current time is more then 30 minutes. If so remove the card, otherwise keep the card.

You can easily create an event or a job to make this happen in your controllers ;)

starmatt's avatar

Hi, thank you both for your answers.

Alright so what I have to most trouble with right know is how to check how old a timestamp is, or how can the framework know that 30 minutes have passed when comparing with a timestamp.

Thanks again!

Edit: I am trying to accomplish this with a schedule (so everyMinute() check if the legacy cart timestamp is 30+ minutes old). If you know a better way to do this with Jobs or Events (which I don't quite know how to use yet), please feel free to give me examples!

ayekoto's avatar

I think using a job is quite easier to scheduling, hope someone help explain how to check time differences cos will also love to learn from this

starmatt's avatar

Hi !

Using scheduling is (to me at least) the most straightforward way for now. I'm pretty sure I'd need a scheduler anyway to check time differences, but as of now I still don't know how to make that check.

What I have now is a timestamp in my user table that gets updated when a new item is added to cart. This would be my legacy timestamp that can hopefully be used to check if 30 minutes have passed, if so and if the cart hasn't been processed, the cart would be deleted. I guess it's not the easiest way, or the fastest either... I'll be sure to keep you updated if I find a way! Thanks for your input though!

frezno's avatar

the question is, why do you want to save the cart in your db on every purchase?
ie why don't you just save it to a session and if the user checks out flush it to the database.
if the user doesn't check out, the session will expire anyway and you don't have to worry about anything.

starmatt's avatar

Hi, the cart gets deleted as well when the purchase has been finalized, and the order is saved in another table. I see your point. But because of reasons irrelevent with my problem, and the way the app needs to work, what I am trying to accomplish is the most convenient and efficient method that I've came up with. Now I just need to make it work :p

frezno's avatar

didn't know the background, was just wondering...

starmatt's avatar

No problem dude!

So, I think I might have found a solution. Here's what my scheduler looks like:

$schedule->call(function() {
    $users = DB::table('users')->get();

    foreach ($users as $user) {

        $legacy_timestamp = \Carbon\Carbon::parse($user->last_cart_timestamp);
        // For some reasons unknown to me I could only retrieve the string format of the timestamp so I had to use Carbon::parse()

        if ($legacy_timestamp != NULL &&
            $legacy_timestamp->diffInMinutes() >= 30) {
        // I am using Carbon diffInMinutes() method which returns an int with the difference between my timestamp and now, I just have to compare this with an int representing the number of minutes I want passed.
                $user_id = $user->id;

                CartController::getEmpty($user_id);
                // Call my "Empty the cart please" method
                DB::table('users')
                    ->where('id', $user_id)
                    ->update(['last_cart_timestamp' => NULL]);
                // Set the timestamp back to NULL
        }
    }
})->everyMinute();      // Check every minute

Feel free to tell me what you guys think! I'll set the topic as resolved once I've tested this thoroughly.

nfauchelle's avatar

@starmatt Did you add last_cart_timestamp to the dates array on your model? https://laravel.com/docs/master/eloquent-mutators#date-mutators

However, I would change this a little, and do something like (untested code).

 $schedule->call(function() {
    $olderThan = \Carbon\Carbon::now()->subMinutes(30);
    $users = User::where('last_cart_timestamp', '<', $olderThan->format('Y-m-d H:i:s'))->get();
    
    foreach($users as $user) {
        CartController::getEmpty($user->id);
        $user->last_cart_timestamp = null;
        $user->save();
    }
    
})->everyMinute();

so we use the DB to look up and find the users, instead of getting all of them and filtering in PHP. Make sure last_cart_timestamp is indexed.

However I still don't like the code.

CartController::getEmpty($user_id); I don't think this should be a controller function. I would rather see a Cart object which this gets called on.

I would also move

    $olderThan = \Carbon\Carbon::now()->subMinutes(30);
    $users = User::where('last_cart_timestamp', '<', $olderThan->format('Y-m-d H:i:s'))->get();

out to a method as well (maybe a query scope on the user. User::getOldSessions().

I would also change it from being every minute to every 5 or 10 that it checks...

Lastly... is 30 minutes to short? I add some stuff to the cart, then go and make a coffee and get my CC before making the purchase and then it's timed out (how long is too long?...).

Good luck.

starmatt's avatar

Hello @nfauchelle, thank you for your answer.

Could you explain your modifications please? And why don't you like that I call a controller function? I'm not trying to question you, just wanna know your thought process and I don't like to copy/paste code I don't fully understand.

I think the change has to be every minute, in the case the difference check isn't enough by a few seconds (ex: timestamp is 9:01:30 and check is at 9:00:00, the next check will occur at 9:05:00 or 9:10:00 and delete the carts 5 or 10 minutes too late which wouldn't be very accurate...)

And yeah, 30 minutes was just set as an example in my OP, I'll have to run tests to find the right balance when going to prod.

Thank you again!

nfauchelle's avatar

@starmatt The main change I did was doing the filtering of the users in the query instead of php.

So the mysql query is, fetch all the users who have a last_cart_timestamp over 30 minutes ago. That way when we process $users we know these ones should be expired.

Your way means getting all users and looping over them, so it's slower and more code.

And why don't you like that I call a controller function?

Well. I would reset the sessions via a cron job / a command, but the code to kill the session is in the CartController. Controllers are for Http requests, so having that code in there is just a bit of a code smell. Sounds like CartController is a bit of a god class.

I think the change has to be every minute, in the case the difference check isn't enough by a few seconds (ex: timestamp is 9:01:30 and check is at 9:00:00, the next check will occur at 9:05:00 or 9:10:00 and delete the carts 5 or 10 minutes too late which wouldn't be very accurate...)

Does it matter? Even PHP sessions are not that accurate. Are you doing some stock holding while it's in the cart type scenario? It'd probably be fine if you are not checking over every single user in PHP...

starmatt's avatar

@nfauchelle Thank you for the explanations, indeed it makes much more sense!

Yes, the point of deleting the carts after a while is to return the items to the stock. I can't have orders processed if the item is not in stock (very limited stock kinda products, won't often be able to bring more in).

I see your point with the Controller method. I haven't worked with laravel Jobs yet, how would you use them? Basically what my getEmpty() method does is:

  • Retrieve the carts corresponding to user->id
  • delete those
  • increment the stock of related items
  • set the 'last_cart_timestamp' to NULL for related user (I removed this from the scheduler last night after posting)
  • return redirect (when the Empty Cart button is clicked in the cart view, dunno if it redirects everybody when checked haha)

I thought it would be simpler to call this function rather than write it again in the scheduler.

By the way, what is your point of view on using table->save() over table->update() ?

nfauchelle's avatar

@starmatt

Yes, the point of deleting the carts after a while is to return the items to the stock. I can't have orders processed if the item is not in stock (very limited stock kinda products, won't often be able to bring more in).

You could possibly do this more simply buy showing the stock count on the product page, allow them to add it to their cart and then on checkout double check there is stock (incase someone else has beat them) and decrease the stock then.

In fact, with the lastest store I did, I didn't decrease the stock until we had actually shipped the item, instead I held the items in a 'reserved' count. So with a product you'd have

Stock: 5 Reserved: 2 (people who have entered their details) Availability: 3 (what is shown on the front end).

We then email them after a few hours if they haven't paid, and then reverse the order if no payment, and then the reserved drops.

I see your point with the Controller method. I haven't worked with laravel Jobs yet, how would you use them? Basically what my getEmpty() method does is: snip

I wouldn't use a Job, what I mean was a command line command, but the code to do that should live in place. Basically the answer to "If I want to be able to run a command line command to clear the expired sessions AND/OR allow people to hit a URL, where should the code go"

By the way, what is your point of view on using table->save() over table->update() ? I haven't looked into the difference, so not sure.

Please or to participate in this conversation.