-
-
Notifications
You must be signed in to change notification settings - Fork 0
Order Workflow Example
Jean-Marc Strauven edited this page Jul 31, 2025
·
2 revisions
A comprehensive e-commerce order processing workflow demonstrating advanced Laravel Statecraft features.
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
pending β paid β processing β shipped β delivered
β β β
cancelled β cancelled β cancelled
- 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)
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
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
<?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;
}
}
<?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;
}
}
<?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
);
}
}
<?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
]);
}
}
<?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()
]);
}
}
<?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']
]);
}
}
<?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';
}
}
<?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');
}
};
<?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));
}
}
}
<?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'));
}
}
<?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
]
]);
}
}
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
- π Article Publishing Example - Content management workflow
- π€ User Subscription Example - Subscription management
- β‘ Event Usage Example - Event-driven patterns
- π― Events System - Event-driven architecture
- π State History - Audit and tracking
- π§ͺ Testing Guide - Testing strategies
- βοΈ Configuration Guide - Advanced configuration
Ready to implement your own order workflow? Start with the π Installation & Setup guide!
π― Laravel Statecraft - Advanced State Machine Implementation for Laravel
Navigate: π Home | π¦ Installation | π Basic Guide | π YAML Config | π‘ Examples
Resources: π‘οΈ Guards & Actions | π― Events | π State History | π§ͺ Testing | π¨ Commands
π Community Resources:
Made with β€οΈ for the Laravel community β’ Contribute β’ License