Debug School

rakesh kumar
rakesh kumar

Posted on

How Laravel's state machine can enhance the automation and management of booking states

Install the Dependency
Install the asantibanez/laravel-eloquent-state-machines package:

composer require asantibanez/laravel-eloquent-state-machines
Enter fullscreen mode Exit fullscreen mode
  1. Configure the State Machine Create the BookingStateMachine class and define state transitions:
namespace App\StateMachines;

use Asantibanez\LaravelEloquentStateMachines\StateMachines\StateMachine;

class BookingStateMachine extends StateMachine
{
 public function transitions(): array
{
    return [
        'Pending' => [
            'status' => 0,
            'transitions' => ['Confirmed', 'Auto-Cancelled']
        ],
        'Confirmed' => [
            'status' => 1,
            'transitions' => ['Paid', 'Auto-Cancelled']
        ],
        'Paid' => [
            'status' => 2,
            'transitions' => ['Vehicle Ready']
        ],
        'Vehicle Ready' => [
            'status' => 3,
            'transitions' => ['In Progress']
        ],
        'In Progress' => [
            'status' => 4,
            'transitions' => ['Completed', 'Issue Reported']
        ],
        'Cancelled' => [
            'status' => 5,
            'transitions' => ['Refunded']
        ],
    ];
}

    public function defaultState(): ?string
    {
        return 'Pending';
    }
}
Enter fullscreen mode Exit fullscreen mode

transitions(): Defines valid state transitions.
defaultState(): Specifies the default state when a new model is created.

Set Up the Model
Update the model to use the state machine:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Asantibanez\LaravelEloquentStateMachines\Traits\HasStateMachines;
use App\StateMachines\BookingStateMachine;

class Booking extends Model
{
    use HasStateMachines;

    protected $fillable = [
        'user_name', 'destination', 'vechicle_type', 'brand', 'model', 'price',
        'start_date', 'end_date', 'number', 'email', 'user_id', 'vender_id',
        'vechicle_image', 'vechicle_id', 'status', 'shop_id', 'state'
    ];

    public $stateMachines = [
        'state' => BookingStateMachine::class,
    ];
}


public function getNumericStatus(): int
{
    $transitions = (new BookingStateMachine)->transitions();
    return $transitions[$this->state]['status'] ?? 0;
}

Enter fullscreen mode Exit fullscreen mode
HasStateMachines: Trait that integrates the state machine into your model.
$stateMachines: Defines which state machine is used for a specific attribute (state).
Enter fullscreen mode Exit fullscreen mode

step4: set state to pending

Modify the Code for Creating a Booking Add the state field when creating a booking record:

$post = Booking::create([
    'user_name' => $user_name,
    'destination' => $Pickupdestination,
    'vechicle_type' => $vechicle_type,
    'brand' => $brand,
    'model' => $model,
    'price' => $price,
    'start_date' => $start_date,
    'end_date' => $end_date,
    'number' => $number,
    'email' => $emails,
    'user_id' => $user_id,
    'vender_id' => $vender_ID,
    'vechicle_image' => json_encode($img),
    'vechicle_id' => $id,
    'status' => '0',
    'shop_id' => $shop_id,
    'state' => 'Pending', // Set initial state
]);
Enter fullscreen mode Exit fullscreen mode

step5: changes to confirmed state

public function change_status(Request $request)
{
    Log::info("Entering the change_status() method in UploadController.");
    Log::info("Request data: " . json_encode($request->all()));

    // Retrieve the booking ID and new state from the request
    $id = $request->input('id');
    $newState = $request->input('state');

    Log::info("Booking ID: {$id}");
    Log::info("New State: {$newState}");

    // Find the booking by ID
    $booking = Booking::find($id);

    if (!$booking) {
        Log::error("Booking with ID {$id} not found.");
        session()->flash('error', 'Booking not found.');
        return back();
    }

    Log::info("Current booking data before update: " . json_encode($booking));

    // Get the current state
    $currentState = $booking->state;

    try {
        switch ($currentState) {
            case 'Pending':
        // Example logic to decide the next state
        $autoCancelCondition = $request->input('auto_cancel', false); // External condition

  $bookings = Booking::where('state', 'Pending')
            ->where('created_at', '<', Carbon::now()->subHour())
            ->get();
        $confirmCondition = $request->input('confirm', false);       // External condition

         if ($autoCancelCondition) {
            $booking->state()->transitionTo('Auto-Cancelled');
            $status = 2;
            Log::info("Booking ID {$id} transitioned to 'Auto-Cancelled'.");
        }
 elseif ($confirmCondition) {
            $booking->state()->transitionTo('Confirmed');
            $status = 1;
            Log::info("Booking ID {$id} transitioned to 'Confirmed'.");
        } else {
            throw new \Exception("No valid condition met for transitioning from Pending.");
        }
        break;

         case 'Confirmed':
    // Example logic to decide the next state
    $paymentSuccessful = $request->input('payment_successful', false); // Condition for payment success
    $autoCancelCondition = $request->input('auto_cancel', false);      // Condition for auto-cancel

    if ($paymentSuccessful) {
        $booking->state()->transitionTo('Paid');
        $status = 3; // Numeric status for Paid
        Log::info("Booking ID {$id} transitioned to 'Paid'.");
    } elseif ($autoCancelCondition) {
        $booking->state()->transitionTo('Auto-Cancelled');
        $status = 2; // Numeric status for Auto-Cancelled
        Log::info("Booking ID {$id} transitioned to 'Auto-Cancelled'.");
    } else {
        throw new \Exception("No valid condition met for transitioning from Confirmed.");
    }
    break;

            case 'Paid':
                if ($newState === 'Vehicle Ready') {
                    $booking->state()->transitionTo($newState);
                    $status = 4;
                } else {
                    throw new \Exception("Invalid transition from Paid to {$newState}");
                }
                break;

            case 'Vehicle Ready':
                if ($newState === 'In Progress') {
                    $booking->state()->transitionTo($newState);
                    $status = 5;
                } else {
                    throw new \Exception("Invalid transition from Vehicle Ready to {$newState}");
                }
                break;

            case 'In Progress':
                if (in_array($newState, ['Completed', 'Issue Reported'])) {
                    $booking->state()->transitionTo($newState);
                    $status = $newState === 'Completed' ? 6 : 7;
                } else {
                    throw new \Exception("Invalid transition from In Progress to {$newState}");
                }
                break;

            case 'Cancelled':
                if ($newState === 'Refunded') {
                    $booking->state()->transitionTo($newState);
                    $status = 8;
                } else {
                    throw new \Exception("Invalid transition from Cancelled to {$newState}");
                }
                break;

            default:
                throw new \Exception("Invalid current state: {$currentState}");
        }

        // Update the status field in the booking record
        $booking->update(['status' => $status]);

        Log::info("Booking ID {$id} transitioned to '{$newState}' with status {$status}.");
        session()->flash('success', 'Status updated successfully.');
    } catch (\Exception $e) {
        Log::error("Error transitioning booking state: " . $e->getMessage());
        session()->flash('error', $e->getMessage());
    }

    return back();
}

Enter fullscreen mode Exit fullscreen mode

How to implement Auto Cancel Logic
add auto_cancelled_at field in model

protected $fillable = [
    'user_name', 'destination', 'vechicle_type', 'brand', 'model', 'price',
    'start_date', 'end_date', 'number', 'email', 'user_id', 'vender_id',
    'vechicle_image', 'vechicle_id', 'status', 'shop_id', 'state',
    'auto_cancelled_at', // Add this field
];
Enter fullscreen mode Exit fullscreen mode

Auto-Cancel Logic
Command to Handle Auto-Cancel
Create a command that transitions stale bookings to Auto-Cancelled and records the auto_cancelled_at timestamp.

php artisan make:command AutoCancelBookings
Enter fullscreen mode Exit fullscreen mode

*Update the generated command *(app/Console/Commands/AutoCancelBookings.php):

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Models\Booking;
use Carbon\Carbon;
use Illuminate\Support\Facades\Log;

class AutoCancelBookings extends Command
{
    protected $signature = 'bookings:auto-cancel';
    protected $description = 'Automatically cancel bookings that have been pending for over 1 hour';

    public function handle()
    {
        Log::info('Starting auto-cancel process for pending bookings.');

        // Find bookings in the 'Pending' state older than 1 hour
        $bookings = Booking::where('state', 'Pending')
            ->where('created_at', '<', Carbon::now()->subHour())
            ->get();

        foreach ($bookings as $booking) {
            try {
                $booking->state()->transitionTo('Auto-Cancelled');
                $booking->update(['auto_cancelled_at' => Carbon::now()]);
                Log::info("Booking ID {$booking->id} auto-cancelled.");
            } catch (\Exception $e) {
                Log::error("Error auto-cancelling Booking ID {$booking->id}: " . $e->getMessage());
            }
        }

        Log::info('Auto-cancel process completed.');
    }
}
Enter fullscreen mode Exit fullscreen mode

Scheduler for Auto-Cancel
Schedule the AutoCancelBookings command in app/Console/Kernel.php:

protected function schedule(Schedule $schedule)
{
    $schedule->command('bookings:auto-cancel')->everyTenMinutes();
}
Enter fullscreen mode Exit fullscreen mode

Add a cron job to your server:

* * * * * php /path-to-your-project/artisan schedule:run >> /dev/null 2>&1
Enter fullscreen mode Exit fullscreen mode
  1. Handle Auto-Cancel in the Booking Creation Process Ensure new bookings start in the Pending state, which the auto-cancel logic handles if they remain unchanged for over an hour.
$post = Booking::create([
    'user_name' => $user_name,
    'destination' => $Pickupdestination,
    'vechicle_type' => $vechicle_type,
    'brand' => $brand,
    'model' => $model,
    'price' => $price,
    'start_date' => $start_date,
    'end_date' => $end_date,
    'number' => $number,
    'email' => $emails,
    'user_id' => $user_id,
    'vender_id' => $vender_ID,
    'vechicle_image' => json_encode($img),
    'vechicle_id' => $id,
    'status' => '0',
    'shop_id' => $shop_id,
    'state' => 'Pending', // Initial state
]);
Enter fullscreen mode Exit fullscreen mode
  1. Testing the Auto-Cancel Logic Test Command Manually run the command to check if bookings are auto-cancelled:
php artisan bookings:auto-cancel
Enter fullscreen mode Exit fullscreen mode

Verify that:

Bookings in the Pending state for over an hour are transitioned to Auto-Cancelled.
The auto_cancelled_at field is updated with the current timestamp.
Test Scheduled Execution
Allow the scheduled command to run and confirm that auto-cancel logic is applied periodically.

Top comments (0)