🧩 Step 9 — Building Order History & Customer Order Tracking
📌 Why This Feature Matters
In any e-commerce store, simply processing a payment isn’t enough.
Customers expect:
- A clear order confirmation page
- A dashboard to review their past orders
- Real-time status tracking (like “Pending”, “Processing”, “Shipped”, “Delivered”)
- Automatic updates if something changes
For the admin/store owner, this also means:
- Having a central system to manage orders
- Being able to update statuses (and notify users)
This builds trust and transparency—key factors for customer loyalty and repeat sales.
🧱 Core Components of Order Tracking System
We’ll build the following:
- Orders Table
- Holds overall order info (user, total, status, payment status, timestamps)
- Order Items Table
- Each product purchased in the order
- Order Status Flow
- Pending → Processing → Shipped → Delivered (with optional “Cancelled” or “Returned”)
- Customer Order History Page
- Users can see their previous orders and details
- Admin Order Management Panel
- Admin can view, filter, and update orders
- Email Notifications (Optional)
- Send confirmation or status update emails
⚙️ Step 1: Database Schema
orders table
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('order_number')->unique();
$table->decimal('total_amount', 10, 2);
$table->enum('status', ['pending','processing','shipped','delivered','cancelled'])->default('pending');
$table->enum('payment_status', ['unpaid','paid'])->default('unpaid');
$table->string('shipping_address');
$table->timestamps();
});
order_items table
Schema::create('order_items', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')->constrained()->onDelete('cascade');
$table->foreignId('product_id')->constrained()->onDelete('cascade');
$table->integer('quantity');
$table->decimal('price', 10, 2);
$table->timestamps();
});
⚙️ Step 2: Models & Relationships
Order.php
class Order extends Model {
protected $fillable = [
'user_id','order_number','total_amount','status','payment_status','shipping_address'
];
public function user() {
return $this->belongsTo(User::class);
}
public function items() {
return $this->hasMany(OrderItem::class);
}
}
OrderItem.php
class OrderItem extends Model {
protected $fillable = [
'order_id','product_id','quantity','price'
];
public function product() {
return $this->belongsTo(Product::class);
}
}
⚙️ Step 3: Placing an Order
When a user checks out:
- Validate cart & payment
- Create an
Orderrecord - Create multiple
OrderItemrecords from cart - Mark payment as
paidif successful
Example snippet inside CheckoutController:
$order = Order::create([
'user_id' => auth()->id(),
'order_number' => 'ORD-' . strtoupper(Str::random(8)),
'total_amount' => $cartTotal,
'payment_status' => 'paid',
'shipping_address' => $request->address,
]);
foreach($cartItems as $item){
OrderItem::create([
'order_id' => $order->id,
'product_id' => $item->product_id,
'quantity' => $item->quantity,
'price' => $item->price
]);
}
⚙️ Step 4: Customer Order History Page
Route:
Route::get('/my-orders', [OrderController::class, 'index'])->middleware('auth');
Controller:
public function index() {
$orders = Order::with('items.product')
->where('user_id', auth()->id())
->latest()
->get();
return view('orders.index', compact('orders'));
}
Blade view (orders/index.blade.php):
<h1>My Orders</h1>
@foreach($orders as $order)
<div class="border p-3 mb-3">
<h3>Order #{{ $order->order_number }}</h3>
<p>Status: {{ ucfirst($order->status) }}</p>
<p>Total: ${{ $order->total_amount }}</p>
<ul>
@foreach($order->items as $item)
<li>{{ $item->product->name }} x {{ $item->quantity }}</li>
@endforeach
</ul>
</div>
@endforeach
⚙️ Step 5: Admin Order Management
Route:
Route::middleware(['auth','admin'])->group(function(){
Route::get('/admin/orders', [AdminOrderController::class, 'index']);
Route::post('/admin/orders/{order}/update-status', [AdminOrderController::class, 'updateStatus']);
});
AdminOrderController:
public function index() {
$orders = Order::with('user')->latest()->get();
return view('admin.orders.index', compact('orders'));
}
public function updateStatus(Request $request, Order $order) {
$order->update(['status' => $request->status]);
return back()->with('success','Order status updated');
}
Blade (admin/orders/index.blade.php):
<table>
<thead>
<tr><th>Order #</th><th>Customer</th><th>Status</th><th>Action</th></tr>
</thead>
<tbody>
@foreach($orders as $order)
<tr>
<td>{{ $order->order_number }}</td>
<td>{{ $order->user->name }}</td>
<td>{{ ucfirst($order->status) }}</td>
<td>
<form method="POST" action="/admin/orders/{{ $order->id }}/update-status">
@csrf
<select name="status" onchange="this.form.submit()">
<option>pending</option>
<option>processing</option>
<option>shipped</option>
<option>delivered</option>
</select>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
⚙️ Step 6: Order Status Tracking (Customer View)
You can enhance the customer experience with a visual order progress bar, e.g.:
@if($order->status == 'pending')
<div>🟡 Pending</div>
@elseif($order->status == 'processing')
<div>🟢 Processing</div>
@elseif($order->status == 'shipped')
<div>📦 Shipped</div>
@elseif($order->status == 'delivered')
<div>✅ Delivered</div>
@endif
For advanced UX, store timestamps for each status change in a order_status_logs table to display a timeline.
⚙️ Step 7: Notifications (Optional)
- Use Laravel Notifications or Mail to send:
- Order confirmation email
- Status update emails (e.g., shipped/delivered)
- Improves trust and keeps customers informed
💡 Additional Tips
- Paginate the customer orders list if they have many.
- Allow invoice download (PDF).
- Add order cancellation request if status is still pending.
- Keep audit logs for admin actions on orders.
✅ Summary
You’ve now built:
- Complete order history for each customer
- Admin dashboard to manage orders
- Real-time status tracking to keep customers updated
This feature makes your store feel truly professional and reliable.