Skip to content

Order Workflow Example

Jean-Marc Strauven edited this page Jul 31, 2025 · 2 revisions

πŸ“¦ Order Workflow Example

A comprehensive e-commerce order processing workflow demonstrating advanced Laravel Statecraft features.

πŸ“– Overview

This example showcases a complete order processing system with:

  • Multi-state order lifecycle - From creation to delivery
  • Payment processing - Secure payment validation and processing
  • Inventory management - Stock validation and reservation
  • Shipping integration - Carrier integration and tracking
  • Customer notifications - Email notifications at each stage
  • Admin controls - Management overrides and cancellations

🎯 Workflow States

State Diagram

pending β†’ paid β†’ processing β†’ shipped β†’ delivered
   ↓        ↓         ↓
cancelled ← cancelled ← cancelled

State Definitions

  • pending - Order created, awaiting payment
  • paid - Payment confirmed, ready for processing
  • processing - Items being prepared for shipment
  • shipped - Order dispatched to customer
  • delivered - Order received by customer
  • cancelled - Order cancelled (from any state)

βš™οΈ YAML Configuration

Complete State Machine Definition

name: OrderStateMachine
model: App\Models\Order
initial_state: pending
description: "Complete e-commerce order processing workflow"

config:
  enable_history: true
  enable_events: true

states:
  - name: pending
    description: "Order created, awaiting payment"
    metadata:
      color: "#FFA500"
      icon: "clock"
      
  - name: paid
    description: "Payment confirmed, ready for processing"
    metadata:
      color: "#00FF00"
      icon: "credit-card"
      
  - name: processing
    description: "Order items being prepared for shipment"
    metadata:
      color: "#0066CC"
      icon: "package"
      
  - name: shipped
    description: "Order dispatched to customer"
    metadata:
      color: "#9900CC"
      icon: "truck"
      
  - name: delivered
    description: "Order received by customer"
    metadata:
      color: "#006600"
      icon: "check-circle"
      
  - name: cancelled
    description: "Order cancelled"
    metadata:
      color: "#CC0000"
      icon: "x-circle"

transitions:
  - name: pay
    description: "Process customer payment"
    from: pending
    to: paid
    guard: PaymentGuard
    action: ProcessPaymentAction
    metadata:
      requires_payment_method: true
      
  - name: process
    description: "Start order processing"
    from: paid
    to: processing
    guards: [InventoryGuard, AddressGuard]
    actions: [ReserveInventoryAction, NotifyWarehouseAction]
    
  - name: ship
    description: "Ship order to customer"
    from: processing
    to: shipped
    guards: [InventoryGuard, ShippingGuard]
    actions: [CreateShipmentAction, SendTrackingEmailAction]
    metadata:
      requires_tracking_number: true
      
  - name: deliver
    description: "Mark order as delivered"
    from: shipped
    to: delivered
    action: MarkAsDeliveredAction
    
  - name: cancel
    description: "Cancel the order"
    from: [pending, paid, processing]
    to: cancelled
    guard: CanCancelGuard
    actions: [CancelOrderAction, ProcessRefundAction, NotifyCustomerAction]
    metadata:
      requires_reason: true

Advanced Guard Expressions

For complex business logic:

transitions:
  - name: expedite
    description: "Fast-track order processing"
    from: paid
    to: processing
    guard:
      and:
        - InventoryGuard
        - or:
          - IsVIPCustomerGuard
          - HasExpressShippingGuard
          - IsUrgentOrderGuard
        - not: IsInternationalOrderGuard
    actions: [ExpediteProcessingAction, NotifyWarehouseAction]
    
  - name: auto_approve_payment
    description: "Automatically approve payment for trusted customers"
    from: pending
    to: paid
    guard:
      and:
        - PaymentGuard
        - IsVerifiedCustomerGuard
        - not:
          - or:
            - IsHighRiskTransactionGuard
            - IsBlacklistedGuard
    action: ProcessPaymentAction

πŸ›‘οΈ Guards Implementation

Payment Validation Guard

<?php

namespace App\Guards;

use Grazulex\LaravelStatecraft\Contracts\Guard;
use Illuminate\Database\Eloquent\Model;
use App\Services\PaymentService;

class PaymentGuard implements Guard
{
    public function __construct(
        private PaymentService $paymentService
    ) {}

    public function passes(Model $model, array $context = []): bool
    {
        // Validate order total
        if ($model->total <= 0) {
            return false;
        }
        
        // Check customer payment method
        if (!$model->customer->hasValidPaymentMethod()) {
            return false;
        }
        
        // Validate payment service availability
        if (!$this->paymentService->isAvailable()) {
            return false;
        }
        
        // Check for fraud indicators
        if ($this->paymentService->isSuspiciousTransaction($model)) {
            return false;
        }
        
        return true;
    }
}

Inventory Validation Guard

<?php

namespace App\Guards;

use Grazulex\LaravelStatecraft\Contracts\Guard;
use Illuminate\Database\Eloquent\Model;
use App\Services\InventoryService;

class InventoryGuard implements Guard
{
    public function __construct(
        private InventoryService $inventoryService
    ) {}

    public function passes(Model $model, array $context = []): bool
    {
        foreach ($model->items as $item) {
            $available = $this->inventoryService->getAvailableStock(
                $item['product_id']
            );
            
            if ($available < $item['quantity']) {
                return false;
            }
        }
        
        return true;
    }
}

VIP Customer Guard

<?php

namespace App\Guards;

use Grazulex\LaravelStatecraft\Contracts\Guard;
use Illuminate\Database\Eloquent\Model;

class IsVIPCustomerGuard implements Guard
{
    public function passes(Model $model, array $context = []): bool
    {
        $customer = $model->customer;
        
        return $customer && (
            $customer->is_vip ||
            $customer->lifetime_value > 10000 ||
            $customer->orders()->count() > 50
        );
    }
}

⚑ Actions Implementation

Payment Processing Action

<?php

namespace App\Actions;

use Grazulex\LaravelStatecraft\Contracts\Action;
use Illuminate\Database\Eloquent\Model;
use App\Services\PaymentService;
use App\Services\NotificationService;

class ProcessPaymentAction implements Action
{
    public function __construct(
        private PaymentService $paymentService,
        private NotificationService $notificationService
    ) {}

    public function execute(Model $model, array $context = []): void
    {
        // Process the payment
        $paymentResult = $this->paymentService->processPayment(
            $model->customer->default_payment_method,
            $model->total,
            [
                'order_id' => $model->id,
                'description' => "Order #{$model->id}",
                'metadata' => $context
            ]
        );
        
        // Update order with payment information
        $model->update([
            'payment_id' => $paymentResult['payment_id'],
            'payment_method' => $paymentResult['payment_method'],
            'payment_processed_at' => now(),
            'payment_fee' => $paymentResult['fee'] ?? 0,
        ]);
        
        // Send confirmation email
        $this->notificationService->sendPaymentConfirmation($model);
        
        // Log the payment
        logger()->info('Payment processed successfully', [
            'order_id' => $model->id,
            'payment_id' => $paymentResult['payment_id'],
            'amount' => $model->total
        ]);
    }
}

Inventory Reservation Action

<?php

namespace App\Actions;

use Grazulex\LaravelStatecraft\Contracts\Action;
use Illuminate\Database\Eloquent\Model;
use App\Services\InventoryService;

class ReserveInventoryAction implements Action
{
    public function __construct(
        private InventoryService $inventoryService
    ) {}

    public function execute(Model $model, array $context = []): void
    {
        $reservations = [];
        
        foreach ($model->items as $item) {
            $reservation = $this->inventoryService->reserve(
                $item['product_id'],
                $item['quantity'],
                [
                    'order_id' => $model->id,
                    'expires_at' => now()->addHours(24)
                ]
            );
            
            $reservations[] = $reservation;
        }
        
        // Store reservation information
        $model->update([
            'inventory_reservations' => $reservations,
            'inventory_reserved_at' => now()
        ]);
    }
}

Shipping Creation Action

<?php

namespace App\Actions;

use Grazulex\LaravelStatecraft\Contracts\Action;
use Illuminate\Database\Eloquent\Model;
use App\Services\ShippingService;
use App\Services\NotificationService;

class CreateShipmentAction implements Action
{
    public function __construct(
        private ShippingService $shippingService,
        private NotificationService $notificationService
    ) {}

    public function execute(Model $model, array $context = []): void
    {
        // Create shipment with carrier
        $shipment = $this->shippingService->createShipment([
            'order_id' => $model->id,
            'recipient' => $model->shipping_address,
            'items' => $model->items,
            'service_type' => $context['service_type'] ?? 'standard',
            'insurance' => $model->total > 500
        ]);
        
        // Update order with shipping information
        $model->update([
            'tracking_number' => $shipment['tracking_number'],
            'carrier' => $shipment['carrier'],
            'service_type' => $shipment['service_type'],
            'estimated_delivery' => $shipment['estimated_delivery'],
            'shipped_at' => now()
        ]);
        
        // Send tracking email to customer
        $this->notificationService->sendShippingNotification($model, [
            'tracking_number' => $shipment['tracking_number'],
            'carrier' => $shipment['carrier'],
            'estimated_delivery' => $shipment['estimated_delivery']
        ]);
    }
}

πŸ“Š Model Implementation

Order Model

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Grazulex\LaravelStatecraft\HasStateMachine;
use Grazulex\LaravelStatecraft\HasStateHistory;

class Order extends Model
{
    use HasFactory, HasStateMachine, HasStateHistory;
    
    protected $stateMachine = 'OrderStateMachine';
    
    protected $fillable = [
        'customer_id',
        'total',
        'items',
        'billing_address',
        'shipping_address',
        'status',
        'payment_id',
        'payment_method',
        'tracking_number',
        'carrier',
        'notes'
    ];
    
    protected $casts = [
        'items' => 'array',
        'billing_address' => 'array',
        'shipping_address' => 'array',
        'inventory_reservations' => 'array',
        'total' => 'decimal:2',
        'payment_fee' => 'decimal:2',
        'payment_processed_at' => 'datetime',
        'inventory_reserved_at' => 'datetime',
        'shipped_at' => 'datetime',
        'delivered_at' => 'datetime'
    ];
    
    // Relationships
    public function customer()
    {
        return $this->belongsTo(User::class, 'customer_id');
    }
    
    public function orderItems()
    {
        return $this->hasMany(OrderItem::class);
    }
    
    // Helper methods
    public function getTotalItemsCount(): int
    {
        return collect($this->items)->sum('quantity');
    }
    
    public function getFormattedTotal(): string
    {
        return '$' . number_format($this->total, 2);
    }
    
    public function isExpressShipping(): bool
    {
        return in_array($this->service_type, ['express', 'overnight']);
    }
    
    public function isInternational(): bool
    {
        return $this->shipping_address['country'] !== 'US';
    }
}

Database Migration

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('orders', function (Blueprint $table) {
            $table->id();
            $table->foreignId('customer_id')->constrained('users');
            $table->decimal('total', 10, 2);
            $table->json('items');
            $table->json('billing_address');
            $table->json('shipping_address');
            $table->string('status')->default('pending');
            
            // Payment information
            $table->string('payment_id')->nullable();
            $table->string('payment_method')->nullable();
            $table->decimal('payment_fee', 8, 2)->default(0);
            $table->timestamp('payment_processed_at')->nullable();
            
            // Inventory information
            $table->json('inventory_reservations')->nullable();
            $table->timestamp('inventory_reserved_at')->nullable();
            
            // Shipping information
            $table->string('tracking_number')->nullable();
            $table->string('carrier')->nullable();
            $table->string('service_type')->default('standard');
            $table->timestamp('estimated_delivery')->nullable();
            $table->timestamp('shipped_at')->nullable();
            $table->timestamp('delivered_at')->nullable();
            
            $table->text('notes')->nullable();
            $table->timestamps();
            
            $table->index(['customer_id', 'status']);
            $table->index('created_at');
        });
    }

    public function down()
    {
        Schema::dropIfExists('orders');
    }
};

🎯 Event Integration

Event Listener

<?php

namespace App\Listeners;

use Grazulex\LaravelStatecraft\Events\StateTransitioned;
use App\Models\Order;
use App\Services\AnalyticsService;
use App\Services\CRMService;

class OrderStateTransitionListener
{
    public function __construct(
        private AnalyticsService $analytics,
        private CRMService $crm
    ) {}

    public function handle(StateTransitioned $event): void
    {
        if (!$event->model instanceof Order) {
            return;
        }

        $order = $event->model;
        
        // Track analytics
        $this->analytics->trackOrderStateChange($order, $event->from, $event->to);
        
        // Update CRM
        $this->crm->updateOrderStatus($order);
        
        // Handle specific transitions
        match ($event->to) {
            'paid' => $this->handlePaymentCompleted($order),
            'shipped' => $this->handleOrderShipped($order),
            'delivered' => $this->handleOrderDelivered($order),
            'cancelled' => $this->handleOrderCancelled($order),
            default => null,
        };
    }
    
    private function handlePaymentCompleted(Order $order): void
    {
        // Send to fulfillment queue
        dispatch(new ProcessOrderJob($order));
        
        // Update customer loyalty points
        $order->customer->addLoyaltyPoints($order->total * 0.01);
    }
    
    private function handleOrderShipped(Order $order): void
    {
        // Schedule delivery tracking
        dispatch(new TrackDeliveryJob($order))->delay(now()->addHours(2));
    }
    
    private function handleOrderDelivered(Order $order): void
    {
        // Request product review
        dispatch(new RequestReviewJob($order))->delay(now()->addDays(3));
        
        // Complete inventory transaction
        dispatch(new CompleteInventoryTransactionJob($order));
    }
    
    private function handleOrderCancelled(Order $order): void
    {
        // Release inventory reservations
        dispatch(new ReleaseInventoryJob($order));
        
        // Process refund if needed
        if ($order->payment_id) {
            dispatch(new ProcessRefundJob($order));
        }
    }
}

πŸ§ͺ Testing

Feature Tests

<?php

namespace Tests\Feature;

use App\Models\Order;
use App\Models\User;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class OrderWorkflowTest extends TestCase
{
    use RefreshDatabase;
    
    public function test_complete_order_workflow()
    {
        $customer = User::factory()->withValidPayment()->create();
        $order = Order::factory()->create([
            'customer_id' => $customer->id,
            'total' => 100.00
        ]);
        
        // Verify initial state
        $this->assertEquals('pending', $order->currentState());
        
        // Process payment
        $this->assertTrue($order->canTransition('pay'));
        $order->transition('pay');
        $this->assertEquals('paid', $order->currentState());
        $this->assertNotNull($order->payment_id);
        
        // Start processing
        $order->transition('process');
        $this->assertEquals('processing', $order->currentState());
        $this->assertNotNull($order->inventory_reserved_at);
        
        // Ship order
        $order->transition('ship', [
            'service_type' => 'express'
        ]);
        $this->assertEquals('shipped', $order->currentState());
        $this->assertNotNull($order->tracking_number);
        
        // Deliver order
        $order->transition('deliver');
        $this->assertEquals('delivered', $order->currentState());
        $this->assertNotNull($order->delivered_at);
    }
    
    public function test_order_cancellation_scenarios()
    {
        $order = Order::factory()->create();
        
        // Can cancel from pending
        $this->assertTrue($order->canTransition('cancel'));
        
        $order->transition('pay');
        // Can cancel after payment (refund)
        $this->assertTrue($order->canTransition('cancel'));
        
        $order->transition('process');
        // Can cancel during processing
        $this->assertTrue($order->canTransition('cancel'));
        
        $order->transition('ship');
        // Cannot cancel once shipped
        $this->assertFalse($order->canTransition('cancel'));
    }
    
    public function test_guard_validations()
    {
        $order = Order::factory()->create(['total' => 0]);
        
        // Cannot pay with zero total
        $this->assertFalse($order->canTransition('pay'));
        
        $order->update(['total' => 100]);
        // Can pay with valid total
        $this->assertTrue($order->canTransition('pay'));
    }
}

πŸ“ˆ Usage Examples

Controller Integration

<?php

namespace App\Http\Controllers;

use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class OrderController extends Controller
{
    public function processPayment(Request $request, Order $order): JsonResponse
    {
        if (!$order->canTransition('pay')) {
            return response()->json([
                'error' => 'Order cannot be paid in current state',
                'current_state' => $order->currentState()
            ], 422);
        }

        try {
            $order->transition('pay', [
                'payment_method' => $request->payment_method,
                'billing_address' => $request->billing_address
            ]);

            return response()->json([
                'message' => 'Payment processed successfully',
                'order' => $order->fresh(),
                'available_transitions' => $order->availableTransitions()
            ]);
        } catch (\Exception $e) {
            return response()->json([
                'error' => 'Payment processing failed: ' . $e->getMessage()
            ], 500);
        }
    }
    
    public function getOrderStatus(Order $order): JsonResponse
    {
        return response()->json([
            'order_id' => $order->id,
            'current_state' => $order->currentState(),
            'available_transitions' => $order->availableTransitions(),
            'history' => $order->stateHistory()->latest()->take(10)->get(),
            'tracking_info' => [
                'tracking_number' => $order->tracking_number,
                'carrier' => $order->carrier,
                'estimated_delivery' => $order->estimated_delivery
            ]
        ]);
    }
}

πŸš€ Next Steps

This comprehensive example demonstrates:

  • Complex state workflows with multiple paths
  • Business logic implementation with guards and actions
  • Event-driven integration with external services
  • Complete testing strategy for state machines
  • Real-world patterns for e-commerce applications

Explore More Examples

Advanced Topics

Ready to implement your own order workflow? Start with the πŸ“– Installation & Setup guide!

Clone this wiki locally