Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

jakubjv's avatar

Cron or if statement for proper validation of confirmation ?

Hello everyone, I have a question. In this component, reservations are saved, and these reservations need to be confirmed by a verification code. Reservations are initially saved as unconfirmed with is_confirmed set to false. When the user enters the code received by email, it is then set to is_confirmed = true. Now, regarding the issue, I want to handle the deletion of unconfirmed reservations within 15 minutes of creation if they are not confirmed with cron job. I initially considered using a cron job for this, but I'm not sure if it's the best solution. I wanted to ask if anyone has any ideas on how to handle this. My goal is that if a reservation is not confirmed, the corresponding available time slot should be returned to the available times, allowing someone else to choose it. I'm unsure whether using a cron job is the best approach or if an if statement with a Rule would be more appropriate.

This is component

<?php

namespace App\Livewire;

use Livewire\Component;
use App\Models\Blacklist;
use App\Models\Reservation;
use App\Mail\ReservationMail;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Mail;
use App\Models\AvailableBusinessHour;

class Booking extends Component
{
    public $full_name;
    public $email;
    public $phone_number;
    public $reservation;

    public $selectedService;
    public $available_business_hour_id;
    public $selectedBusinessHourId;
    public $verification_code;
    public $successMessage;
    public $successMessageVisible;
    public $successMessageTimeout = 3000;
    public $verificationCodeSubmitted = false;
    public $successMessageClosed = false;


    public function getAvailableBusinessHours()
    {
        return AvailableBusinessHour::whereDoesntHave('reservation')
            ->where(function ($query) {
                $query->where('day', '>', now()->format('Y-m-d'))
                    ->orWhere(function ($query) {
                        $query->where('day', '=', now()->format('Y-m-d'))
                            ->where('from', '>', now()->format('H:i'));
                    });
            })
            ->orderBy('day')
            ->orderBy('from')
            ->get();
    }



    public function render()
    {
        $this->available_business_hour_id = $this->getAvailableBusinessHours();
        return view('livewire.booking', [
            'emptyOption' => 'Vyber si čas rezervace',
        ]);
    }

    public function store()
    {
        $this->validate([
            'full_name' => 'required|string|max:250',
            'email' => [
                'required',
                'email',
                'max:250',
                Rule::unique('blacklist', 'email')->where(function ($query) {
                    return $query->where('email', $this->email);
                }),
            ],
            'phone_number' => 'required|phone:CZ',
            'selectedService' => 'required',
            'selectedBusinessHourId' => 'required|unique:reservations,available_business_hour_id'
        ], [
            'email.unique' => 'Pod tímto e-mailem došlo k vytvoření podezřelého počtu rezervací a byl dán na blacklist, pokud se chcete zarezervovat tak pouze telefonicky.',
            'selectedBusinessHourId.unique' => 'Tento termín byl již obsazen, vyber jiný pokud je nějaký dostupný.',
            'selectedBusinessHourId.required' => 'Výběr času objednání je povinné pole.',
            'phone_number.phone' => 'Chybný formát telefonního čísla.',
            'selectedService.required' => 'Toto je povinné pole',
        ]);

        $selectedBusinessHour = AvailableBusinessHour::find($this->selectedBusinessHourId);

        if (!$selectedBusinessHour) {
            $this->addError('selectedBusinessHourId', 'Vybraný čas není dostupný.');
            return;
        }

        if (
            $selectedBusinessHour->day < now()->format('Y-m-d') ||
            ($selectedBusinessHour->day == now()->format('Y-m-d') && $selectedBusinessHour->from < now()->format('H:i'))
        ) {
            $this->addError('selectedBusinessHourId', 'Tento termín již není dostupný, vyberte jiný.');
            return;
        }


        $verification_code = rand(100000, 999999);

        $existingReservation = Reservation::where('available_business_hour_id', $this->selectedBusinessHourId)
            ->where('is_confirmed', false)
            ->first();

        if ($existingReservation) {
            $this->addError('selectedBusinessHourId', 'Tento termín byl již obsazen, vyberte jiný pokud je nějaký dostupný.');
            return;
        }

        $isBlacklisted = Blacklist::where('email', $this->email)->exists();

        if ($isBlacklisted) {
            $this->addError('email', 'Tento e-mail je na blacklistu a nelze provést rezervaci.');
            return;
        }

        $reservation = Reservation::create([
            'full_name' => $this->full_name,
            'email' => $this->email,
            'phone_number' => $this->phone_number,
            'service' => $this->selectedService,
            'available_business_hour_id' => $this->selectedBusinessHourId,
            'day' => $selectedBusinessHour['day'],
            'from' => $selectedBusinessHour['from'],
            'to' => $selectedBusinessHour['to'],
            'verification_code' => $verification_code
        ]);

        session(['verification_code' => $verification_code, 'current_reservation_id' => $reservation->id]);


        $this->sendReservationConfirmationMail($reservation);


        $this->dispatch('show-verification-modal');
        $this->dispatch('update-success-message', ['closed' => $this->successMessageClosed]);


        $this->reset(['full_name', 'email', 'phone_number', 'available_business_hour_id']);
    }

    public function sendReservationConfirmationMail($reservation)
    {
        Mail::to($reservation->email)->send(new ReservationMail($reservation));
    }

    public function services()
    {
        return ["Stříhání vlasů", "Stříhání + úprava vousů", "Barvení vousů", "Junior střih", "Úprava vousů"];
    }

    public function confirmReservation()
    {
        $this->validate([
            'verification_code' => 'required|digits:6',
        ], [
            'verification_code.required' => 'Zadejte prosím verifikační kód.',
            'verification_code.digits' => 'Verifikační kód musí mít 6 čísel.',
        ]);

        $user_entered_code = $this->verification_code;

        $reservation = Reservation::where('verification_code', $user_entered_code)
            ->where('is_confirmed', false)
            ->where('id', session('current_reservation_id'))
            ->latest()
            ->first();

        if ($reservation) {
            $reservation->update(['is_confirmed' => true]);
            $this->reset(['verification_code']);
            $this->successMessage = 'Rezervace proběhla úspěšně.';
            $this->successMessageVisible = true;
            $this->dispatch('hide-verification-modal');
            $this->dispatch('update-success-message', ['closed' => $this->successMessageClosed]);
        } else {
            $this->addError('verification_code', 'Neplatný verifikační kód nebo rezervace již byla potvrzena.');
        }
    }
}
0 likes
14 replies
gych's avatar

A cron job can be a solution to solve this but its not ideal to make separate cron jobs for each reservation. You could use a cron job that checks the db for multiple reservations that are not confirmed within the 15 minutes but then that cron job would have to run more frequently than 15 minutes.

Another option is to save the new reservation to cache for 15min and link it to a unique cache key. If its not confirmed within those 15 minutes than it will be removed from the cache and the slot will be free again.

By using cache its not a must to store the data in the DB at creation of the reservation. When the user confirms the reservation you can use the data from the cache and add that to the DB.

If you prefer to add the reservation to the DB together with adding it as cache you can also do that and make a cron job that checks daily for unconfirmed reservation, its up to you what you prefer.

1 like
jakubjv's avatar

@gych i didn't ever try to save anything to cache, so if i will do this through cache, it means i dont need any cron job or anything like this?

kiwi0134's avatar

@jakubjv Adding a cache adds another layer of complexity. Using a cache is a totally valid solution for this problem. Depending on your cache driver (but most support it), you can set when an entry should expire. Refer to the documentation for how to use this: https://laravel.com/docs/10.x/cache#storing-items-in-the-cache

If you still want to use the DB, which is also totally valid, you can create an Artisan command that does that and use that together with Laravels Task Scheduler to prune old entries every minute. This can actually be done with a single SQL statement, like (pseudocode) delete * from reservations where created_at < now - 15 minutes. If you have cleanup tasks hooked to observers, you should chunk and iterate them instead, of course.

Here's some documentation about scheduling: https://laravel.com/docs/10.x/scheduling#main-content

1 like
jakubjv's avatar

@kiwi0134 i got this for that

<?php

namespace App\Console\Commands;

use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class DeleteUnconfirmedRes extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'delete-unconfirmed-res';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Execute the console command.
     */
    public function handle()
{
    $now = Carbon::now();

    DB::table('reservations')
        ->where('is_confirmed', '=', false)
        ->where('created_at', '<', $now->subMinutes(15))
        ->delete();

    Log::info('Expired reservations deleted successfully');



}
}

and it works, but i am just not sure if is it good run this every 15 minutes you know what i mean :/, but thanks for advice i will check it aswell

gych's avatar

@jakubjv Yes indeed if you add it to the cache and only save the reservation to the DB when confirmed you don't need a cron job for this. The cache holds all the data for 15 minutes.

In the Laravel documentation you will find detailed information on how to use the cache: https://laravel.com/docs/10.x/cache

Here is a small example of how this can work in your case.

Add the Cache Facade at the top in your file

use Illuminate\Support\Facades\Cache;

Add the data to the cache. The reservationNumber has to be an unique value, you can for example use a random generated number/token for it.

// When creating a new reservation
$reservation = //  your reservation data also add $reservationNumber to this data
$cacheKey = 'unconfirmed_reservation_' . $reservationNumber;

Cache::put($cacheKey, $reservation, now()->addMinutes(15));

Then when the user confirms the reservation check if the cache still holds the data and has not expired yet.

$cacheKey = 'unconfirmed_reservation_' . $reservationNumber;

// Check if reservation exists in cache and is unconfirmed
if (Cache::has($cacheKey)) {

	$reservation = Cache::get($cacheKey);

	/* Add reservation to the DB + also add the reservationNumber to it, you will need this to check if the reservation hasn't been already confirmed when the number is not found in cache */

    // Remove reservation from cache
    Cache::forget($cacheKey);
} else {
	/* Check if reservation doesn't already exists in database, if not the reservation has not been confirmed in time. Send message to inform the user */
}

1 like
gych's avatar

@jakubjv By running DeleteUnconfirmedRes every 15 minutes it might be possible that the unconfirmed reservation is not always deleted after 15 min.

So like I said in my first reply you can use this method but you will have to run it more frequently like every minute

For example

  • Reservation made at 21:05
  • DeleteUnconfirmedRes to check unconfirmed reservation runs at 21:15 the previous created reservation is not expired because it has only been made 10 min before the con job runs
  • DeleteUnconfirmedRes runs at 21:30, now it will delete the reservation if unconfirmed but it has already been expired for 10 min So in total the reservation slot was not available for 25 minutes
1 like
jakubjv's avatar

@gych Yeah, you got true, understand that very vell, and tahnk you very very much for advice, now unfortunately I don't have enough time to try cache, but tomorrow i will let you know if it works ! :)

gych's avatar

@jakubjv Take your time, If you have difficulties figuring it out, don't hesitate to reach out.

1 like
kiwi0134's avatar
kiwi0134
Best Answer
Level 8

@jakubjv You can definitely run a cronjob every minute. Then the worst that can happen is that a reservation is deleted after 15:59 minutes instead of 15 minutes. If that's OK for you, go for it. A cron that runs every minute just results in 1440 executions a day and while this sounds much, it really isn't. Don't worry about it, as you're not doing any complex logic in there.

One potential caveat I see with the cache solution provided by @gych: You probably have to merge your saved reservations with cached reservations to get all still available seats (= whatever the user can reserve). This can be tricky. Don't overoptimise something if you don't have to. Only do whats necessary and don't do anything that could be useful in the future, as that just increases the maintenance cost (in whatever unit you want to measure it).

I'd simply use the database until that doesn't work for you anymore. Also, if you'd need to cleanup anything after a reservation expires, you couldn't do that with a cache as it's not notifying your application about it. If you save and query them from the DB, you can do whatever you want with a to-be-deleted-reservation.

If you absolutely want to go with the cache solution, I'd like to propose a refactoring to the code provided by @gych:

if (is_null($reservation = Cache::get('reservation-cache-key'))) {
    // Logic to fail the request. $reservation is null
}

// Reservation still exists, it is not stored inside $reservation
// Don't forget to remove (= forget) this reservation from the cache

This removes the need to query the cache multiple times. All the has method does is trying to get the item from the cache and then check if it's null. You can do that in one line here and have the data ready. This depends on your code style, though. Do what looks better for you.

1 like
jakubjv's avatar

@kiwi0134 I gavea chance to cache, but for me is really better solution cron. Thanks for advice.

1 like
jakubjv's avatar

@gych So i tested both solutions, but cron every minute seats best for me, but I am glad for your advices, probably i will use this solutione with cache in another project! :) thank you :)

2 likes
gych's avatar

@jakubjv I'm glad you found the solution that fits best with your project.

1 like
kiwi0134's avatar

@jakubjv Happy to hear that and thank you very much for the best answer badge :)

And remember: Nothing is permanent. If your application booms with great success and the cronjob solution doesn't fit your needs anymore, you can still refactor it later to whatever fits best :) This, of course, comes with probably a lot of refactoring and migration work.

Just a thought: If you don't do any very consuming tasks in your cron, it should be able to handle even thousands of records every minute. But if you do need to do time consuming stuff when cleaning up expirations, you might want to outsource those to jobs that you can dispatch from your cronjob. You can also dispatch them to a different queue and dedicate some workers to that queue, so those jobs aren't blocked by other jobs.

Snapey's avatar

I would definitely use a cron job.

Run it every minute. Query all reservations that are more than 15 minutes old and delete them.

This could be as simple as a single database update statement run once per minute.

1 like

Please or to participate in this conversation.