Skip to content

Commit 8a28b3f

Browse files
committed
feat: implement customer checkout and order management
- Add CheckoutService for processing cart-to-order conversion - Add CheckoutController with checkout page and process endpoints - Add OrderController for customer order listing and details - Add ProcessCheckoutRequest for checkout validation - Extend OrderService with getCustomerOrders method - Register checkout and order routes in customer route group
1 parent c999254 commit 8a28b3f

6 files changed

Lines changed: 357 additions & 0 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Customer;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Http\Requests\Checkout\ProcessCheckoutRequest;
7+
use App\Services\Customer\CheckoutService;
8+
use Illuminate\Support\Facades\Auth;
9+
use Inertia\Inertia;
10+
11+
class CheckoutController extends Controller
12+
{
13+
/**
14+
* Inject checkout service
15+
*
16+
* @param CheckoutService $checkoutService
17+
*/
18+
public function __construct(
19+
protected CheckoutService $checkoutService
20+
) {}
21+
22+
public function index()
23+
{
24+
try {
25+
$summary = $this->checkoutService->getCheckoutSummary(Auth::id());
26+
27+
return Inertia::render('customer/Checkout', $summary);
28+
} catch (\Exception $e) {
29+
return $this->flashError($e->getMessage(), 'customer.cart.index');
30+
}
31+
}
32+
33+
/**
34+
* Process checkout
35+
*/
36+
public function process(ProcessCheckoutRequest $request)
37+
{
38+
try {
39+
$orderId = $this->checkoutService->processCheckout(
40+
Auth::id(),
41+
$request->validated()
42+
);
43+
44+
return $this->flashSuccess(
45+
'Order placed successfully!',
46+
'customer.orders.show',
47+
['id' => $orderId]
48+
);
49+
} catch (\Exception $e) {
50+
return $this->flashError($e->getMessage(), 'customer.checkout.index');
51+
}
52+
}
53+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Customer;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Services\OrderService;
7+
use Illuminate\Http\Request;
8+
use Illuminate\Support\Facades\Auth;
9+
use Inertia\Inertia;
10+
11+
class OrderController extends Controller
12+
{
13+
/**
14+
* Inject Order Service
15+
*
16+
* @param OrderService $orderService
17+
*/
18+
public function __construct(
19+
protected OrderService $orderService
20+
) {}
21+
22+
/**
23+
* Display customer's orders
24+
*/
25+
public function index(Request $request)
26+
{
27+
$filters = $request->only([
28+
'status',
29+
'payment_method',
30+
'date_from',
31+
'date_to'
32+
]);
33+
34+
$orders = $this->orderService->getCustomerOrders(
35+
Auth::id(),
36+
10,
37+
$filters
38+
);
39+
40+
return Inertia::render('customer/Orders/Index', [
41+
'orders' => $orders,
42+
'filters' => $filters
43+
]);
44+
}
45+
46+
/**
47+
* Display single order details
48+
*/
49+
public function show(int $id)
50+
{
51+
$order = $this->orderService->find($id);
52+
53+
// Ensure customer can only view their own orders
54+
if (! $order || $order->user_id !== Auth::id()) {
55+
return $this->flashError('Order not found', 'customer.orders.index');
56+
}
57+
58+
return Inertia::render('customer/Orders/Show', [
59+
'order' => $order
60+
]);
61+
}
62+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace App\Http\Requests\Checkout;
4+
5+
use App\Enums\UserRole;
6+
use Illuminate\Foundation\Http\FormRequest;
7+
use Illuminate\Support\Facades\Auth;
8+
9+
class ProcessCheckoutRequest extends FormRequest
10+
{
11+
/**
12+
* Determine if the user is authorized to make this request.
13+
*/
14+
public function authorize(): bool
15+
{
16+
return Auth::check() && Auth::user()->role === UserRole::Customer;
17+
}
18+
19+
/**
20+
* Get the validation rules that apply to the request.
21+
*
22+
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
23+
*/
24+
public function rules(): array
25+
{
26+
return [
27+
'payment_method' => 'required|string|in:cash,gcash,bank_transfer',
28+
'delivery_method' => 'required|string|in:pickup,delivery',
29+
'delivery_address' => 'required_if:delivery_method,delivery|nullable|string|max:500',
30+
];
31+
}
32+
33+
/**
34+
* Get custom validation messages
35+
*
36+
* @return array<string, string>
37+
*/
38+
public function messages(): array
39+
{
40+
return [
41+
'payment_method.required' => 'Please select a payment method.',
42+
'payment_method.in' => 'Invalid payment method selected.',
43+
'delivery_method.required' => 'Please select a delivery method.',
44+
'delivery_method.in' => 'Invalid delivery method selected.',
45+
'delivery_address.required_if' => 'Delivery address is required for delivery orders.',
46+
];
47+
}
48+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php
2+
3+
namespace App\Services\Customer;
4+
5+
use App\Repositories\Interfaces\CartRepositoryInterface;
6+
use App\Repositories\Interfaces\ItemRepositoryInterface;
7+
use App\Repositories\Interfaces\OrderRepositoryInterface;
8+
use Illuminate\Support\Facades\DB;
9+
10+
class CheckoutService
11+
{
12+
/**
13+
* Inject required repositories
14+
*
15+
* @param CartRepositoryInterface $cartRepo
16+
* @param OrderRepositoryInterface $orderRepo
17+
* @param ItemRepositoryInterface $itemRepo
18+
*/
19+
public function __construct(
20+
protected CartRepositoryInterface $cartRepo,
21+
protected OrderRepositoryInterface $orderRepo,
22+
protected ItemRepositoryInterface $itemRepo
23+
) {}
24+
25+
/**
26+
* Process checkout and create order from cart
27+
*
28+
* @param int $userId
29+
* @param array $checkoutData
30+
* @return int Order ID
31+
* @throws \Exception
32+
*/
33+
public function processCheckout(int $userId, array $checkoutData): int
34+
{
35+
// Validate cart has items
36+
$cartItems = $this->cartRepo->getUserCart($userId);
37+
38+
if ($cartItems->isEmpty()) {
39+
throw new \Exception('Your cart is empty');
40+
}
41+
42+
// Validate stock availability
43+
$this->validateStock($cartItems);
44+
45+
// Calculate totals
46+
$totalAmount = $this->calculateTotal($cartItems);
47+
48+
DB::beginTransaction();
49+
try {
50+
// Create order
51+
$order = $this->orderRepo->create([
52+
'user_id' => $userId,
53+
'status' => 'pending',
54+
'payment_method' => $checkoutData['payment_method'],
55+
'delivery_method' => $checkoutData['delivery_method'],
56+
'delivery_address' => $checkoutData['delivery_address'] ?? null,
57+
'total_amount' => $totalAmount,
58+
]);
59+
60+
// Create order items and deduct stock
61+
foreach ($cartItems as $cartItem) {
62+
// Create order item
63+
$this->orderRepo->query()
64+
->find($order->id)
65+
->orderItems()
66+
->create([
67+
'item_id' => $cartItem->item_id,
68+
'quantity' => $cartItem->quantity,
69+
'unit_price' => $cartItem->price
70+
]);
71+
72+
// Deduct stock from product
73+
$product = $this->itemRepo->find($cartItem->item_id);
74+
$this->itemRepo->update($product->id, [
75+
'quantity' => $product->quantity - $cartItem->quantity
76+
]);
77+
}
78+
79+
// Clear cart after successful order
80+
$this->cartRepo->clearUserCart($userId);
81+
82+
DB::commit();
83+
84+
return $order->id;
85+
} catch (\Exception $e) {
86+
DB::rollBack();
87+
throw new \Exception('Failed to process checkout. Please try again.');
88+
}
89+
}
90+
91+
/**
92+
* Validate stock availability for cart items
93+
*
94+
* @param \Illuminate\Database\Eloquent\Collection $cartItems
95+
* @return void
96+
* @throws \Exception
97+
*/
98+
protected function validateStock($cartItems): void
99+
{
100+
foreach ($cartItems as $cartItem) {
101+
$product = $this->itemRepo->find($cartItem->item_id);
102+
103+
if (! $product) {
104+
throw new \Exception('Product not found');
105+
}
106+
107+
if ($product->quantity < $cartItem->quantity) {
108+
throw new \Exception('Insufficient stock');
109+
}
110+
}
111+
}
112+
113+
/**
114+
* Calculate total amount from cart items
115+
*
116+
* @param \Illuminate\Database\Eloquent\Collection $cartItems
117+
* @return float
118+
*/
119+
protected function calculateTotal($cartItems): float
120+
{
121+
return $cartItems->sum(function ($cartItem) {
122+
return $cartItem->price * $cartItem->quantity;
123+
});
124+
}
125+
126+
/**
127+
* Get checkout summary
128+
*
129+
* @param int $userId
130+
* @return array
131+
* @throws \Exception
132+
*/
133+
public function getCheckoutSummary(int $userId): array
134+
{
135+
$cartItems = $this->cartRepo->getUserCart($userId);
136+
137+
if ($cartItems->isEmpty()) {
138+
throw new \Exception('Your cart is empty');
139+
}
140+
141+
$subTotal = $this->calculateTotal($cartItems);
142+
143+
return [
144+
'items' => $cartItems,
145+
'subTotal' => $subTotal,
146+
'total' => $subTotal,
147+
'item_count' => $cartItems->sum('quantity')
148+
];
149+
}
150+
}

app/Services/OrderService.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,34 @@ public function find(int $id)
5656
->find($id);
5757
}
5858

59+
/**
60+
* Get paginated orders for a specific customer
61+
*
62+
* @param int $userId
63+
* @param int $perPage
64+
* @param array $filters
65+
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
66+
*/
67+
public function getCustomerOrders(int $userId, int $perPage = 10, array $filters = [])
68+
{
69+
$query = $this->orderRepo->query()
70+
->with(['orderItems.item'])
71+
->where('user_id', $userId);
72+
73+
$this->applyExactFilter($query, 'status', $filters['status'] ?? null);
74+
$this->applyExactFilter($query, 'payment_method', $filters['payment_method'] ?? null);
75+
76+
if (! empty($filters['date_from'])) {
77+
$query->whereDate('created_at', '>=', $filters['date_from']);
78+
}
79+
80+
if (! empty($filters['date_to'])) {
81+
$query->whereDate('created_at', '<=', $filters['date_to']);
82+
}
83+
84+
return $query->orderBy('created_at', 'desc')->paginate($perPage);
85+
}
86+
5987
/**
6088
* Update order status
6189
*

routes/customer.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
use App\Enums\UserRole;
44
use App\Http\Controllers\Customer\CartController;
5+
use App\Http\Controllers\Customer\CheckoutController;
56
use App\Http\Controllers\Customer\HomepageController;
7+
use App\Http\Controllers\Customer\OrderController;
68
use Illuminate\Support\Facades\Route;
79

810
Route::middleware(['auth', 'verified', 'role:' . UserRole::Customer->value])
@@ -22,5 +24,19 @@
2224

2325
Route::delete('/carts-clear', [CartController::class, 'clear'])->name('carts.clear');
2426

27+
Route::prefix('checkout')->name('checkout.')->group(function () {
28+
Route::controller(CheckoutController::class)->group(function () {
29+
Route::get('/', 'index')->name('index');
30+
Route::post('/process', 'process')->name('process');
31+
});
32+
});
33+
34+
Route::prefix('orders')->name('orders.')->group(function () {
35+
Route::controller(OrderController::class)->group(function () {
36+
Route::get('/', 'index')->name('index');
37+
Route::get('/{id}', 'show')->name('show');
38+
});
39+
});
40+
2541
require __DIR__ . '/settings.php';
2642
});

0 commit comments

Comments
 (0)