Skip to content

Commit 0ea9c6f

Browse files
committed
Phases 226-230: E-commerce Module — 25 tests passing
5 migrations (store_settings, store_categories, store_products bridge, store_orders, store_order_items), 5 models (StoreSettings/Category/Product with isOnSale()/discountPercent(), StoreOrder with SO-YYYY-NNNNN + confirm/markPaid/ ship/deliver/cancel, StoreOrderItem), EcommercePolicy, 6 controllers (Dashboard/ Settings/Category/Product/Order/Storefront), 13 React pages (admin CRUD + public storefront at /store/{slug} with no auth), Sidebar E-commerce section. 25/25 feature tests passing. https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent 91fc1eb commit 0ea9c6f

35 files changed

Lines changed: 3532 additions & 0 deletions

erp/app/Modules/Core/Providers/CoreServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use App\Modules\Marketing\Providers\MarketingServiceProvider;
2020
use App\Modules\FieldService\Providers\FieldServiceProvider;
2121
use App\Modules\Approvals\Providers\ApprovalsServiceProvider;
22+
use App\Modules\Ecommerce\Providers\EcommerceServiceProvider;
2223
use Illuminate\Support\Facades\Gate;
2324
use Illuminate\Support\ServiceProvider;
2425

@@ -39,6 +40,7 @@ public function register(): void
3940
$this->app->register(MarketingServiceProvider::class);
4041
$this->app->register(FieldServiceProvider::class);
4142
$this->app->register(ApprovalsServiceProvider::class);
43+
$this->app->register(EcommerceServiceProvider::class);
4244
}
4345

4446
public function boot(): void
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace App\Modules\Ecommerce\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\Ecommerce\Models\StoreOrder;
7+
use App\Modules\Ecommerce\Models\StoreOrderItem;
8+
use Illuminate\Support\Facades\DB;
9+
use Inertia\Inertia;
10+
use Inertia\Response;
11+
12+
class EcommerceDashboardController extends Controller
13+
{
14+
public function index(): Response
15+
{
16+
$tenantId = auth()->user()->tenant_id;
17+
18+
$totalOrders = StoreOrder::where('tenant_id', $tenantId)->count();
19+
20+
$ordersToday = StoreOrder::where('tenant_id', $tenantId)
21+
->whereDate('created_at', today())
22+
->count();
23+
24+
$revenueToday = StoreOrder::where('tenant_id', $tenantId)
25+
->whereDate('created_at', today())
26+
->where('payment_status', 'paid')
27+
->sum('total');
28+
29+
$pendingOrders = StoreOrder::where('tenant_id', $tenantId)
30+
->where('status', 'pending')
31+
->count();
32+
33+
$topProducts = StoreOrderItem::select('product_name', DB::raw('SUM(quantity) as order_count'))
34+
->whereHas('order', fn ($q) => $q->where('tenant_id', $tenantId))
35+
->groupBy('product_name')
36+
->orderByDesc('order_count')
37+
->limit(5)
38+
->get();
39+
40+
$recentOrders = StoreOrder::where('tenant_id', $tenantId)
41+
->latest()
42+
->limit(10)
43+
->get()
44+
->map(fn ($o) => [
45+
'id' => $o->id,
46+
'order_number' => $o->order_number,
47+
'customer_name' => $o->customer_name,
48+
'total' => $o->total,
49+
'status' => $o->status,
50+
'payment_status' => $o->payment_status,
51+
'created_at' => $o->created_at,
52+
]);
53+
54+
return Inertia::render('Ecommerce/Dashboard', [
55+
'stats' => [
56+
'total_orders' => $totalOrders,
57+
'orders_today' => $ordersToday,
58+
'revenue_today' => (float) $revenueToday,
59+
'pending_orders' => $pendingOrders,
60+
],
61+
'topProducts' => $topProducts,
62+
'recentOrders' => $recentOrders,
63+
]);
64+
}
65+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
namespace App\Modules\Ecommerce\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\Ecommerce\Models\StoreCategory;
7+
use Illuminate\Http\RedirectResponse;
8+
use Illuminate\Http\Request;
9+
use Illuminate\Support\Str;
10+
use Inertia\Inertia;
11+
use Inertia\Response;
12+
13+
class StoreCategoryController extends Controller
14+
{
15+
public function index(): Response
16+
{
17+
$tenantId = auth()->user()->tenant_id;
18+
19+
$categories = StoreCategory::where('tenant_id', $tenantId)
20+
->withCount('storeProducts')
21+
->orderBy('sort_order')
22+
->orderBy('name')
23+
->get()
24+
->map(fn ($c) => [
25+
'id' => $c->id,
26+
'name' => $c->name,
27+
'slug' => $c->slug,
28+
'description' => $c->description,
29+
'parent_id' => $c->parent_id,
30+
'sort_order' => $c->sort_order,
31+
'is_active' => $c->is_active,
32+
'store_products_count' => $c->store_products_count,
33+
]);
34+
35+
return Inertia::render('Ecommerce/Categories/Index', [
36+
'categories' => $categories,
37+
]);
38+
}
39+
40+
public function store(Request $request): RedirectResponse
41+
{
42+
$tenantId = auth()->user()->tenant_id;
43+
44+
$data = $request->validate([
45+
'name' => 'required|string|max:255',
46+
'description' => 'nullable|string',
47+
'parent_id' => 'nullable|exists:store_categories,id',
48+
'sort_order' => 'nullable|integer|min:0',
49+
'is_active' => 'boolean',
50+
]);
51+
52+
$slug = Str::slug($data['name']);
53+
$originalSlug = $slug;
54+
$counter = 1;
55+
while (StoreCategory::where('tenant_id', $tenantId)->where('slug', $slug)->exists()) {
56+
$slug = $originalSlug . '-' . $counter++;
57+
}
58+
59+
StoreCategory::create(array_merge($data, [
60+
'tenant_id' => $tenantId,
61+
'slug' => $slug,
62+
]));
63+
64+
return redirect()->back()->with('success', 'Category created.');
65+
}
66+
67+
public function update(Request $request, StoreCategory $category): RedirectResponse
68+
{
69+
$data = $request->validate([
70+
'name' => 'required|string|max:255',
71+
'description' => 'nullable|string',
72+
'parent_id' => 'nullable|exists:store_categories,id',
73+
'sort_order' => 'nullable|integer|min:0',
74+
'is_active' => 'boolean',
75+
]);
76+
77+
$category->update($data);
78+
79+
return redirect()->back()->with('success', 'Category updated.');
80+
}
81+
82+
public function destroy(StoreCategory $category): RedirectResponse
83+
{
84+
$category->delete();
85+
86+
return redirect()->back()->with('success', 'Category deleted.');
87+
}
88+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
namespace App\Modules\Ecommerce\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\Ecommerce\Models\StoreOrder;
7+
use Illuminate\Http\RedirectResponse;
8+
use Illuminate\Http\Request;
9+
use Inertia\Inertia;
10+
use Inertia\Response;
11+
12+
class StoreOrderController extends Controller
13+
{
14+
public function index(Request $request): Response
15+
{
16+
$tenantId = auth()->user()->tenant_id;
17+
18+
$orders = StoreOrder::withCount('items')
19+
->where('tenant_id', $tenantId)
20+
->when($request->status, fn ($q) => $q->where('status', $request->status))
21+
->when($request->payment_status, fn ($q) => $q->where('payment_status', $request->payment_status))
22+
->latest()
23+
->paginate(25)
24+
->through(fn ($o) => [
25+
'id' => $o->id,
26+
'order_number' => $o->order_number,
27+
'customer_name' => $o->customer_name,
28+
'customer_email' => $o->customer_email,
29+
'items_count' => $o->items_count,
30+
'total' => $o->total,
31+
'status' => $o->status,
32+
'payment_status' => $o->payment_status,
33+
'created_at' => $o->created_at,
34+
]);
35+
36+
return Inertia::render('Ecommerce/Orders/Index', [
37+
'orders' => $orders,
38+
'filters' => $request->only(['status', 'payment_status']),
39+
]);
40+
}
41+
42+
public function show(StoreOrder $order): Response
43+
{
44+
$order->load(['items.storeProduct.product', 'processedBy']);
45+
46+
return Inertia::render('Ecommerce/Orders/Show', [
47+
'order' => [
48+
'id' => $order->id,
49+
'order_number' => $order->order_number,
50+
'status' => $order->status,
51+
'customer_name' => $order->customer_name,
52+
'customer_email' => $order->customer_email,
53+
'customer_phone' => $order->customer_phone,
54+
'shipping_address' => $order->shipping_address,
55+
'billing_address' => $order->billing_address,
56+
'subtotal' => $order->subtotal,
57+
'discount_amount' => $order->discount_amount,
58+
'shipping_amount' => $order->shipping_amount,
59+
'tax_amount' => $order->tax_amount,
60+
'total' => $order->total,
61+
'payment_method' => $order->payment_method,
62+
'payment_status' => $order->payment_status,
63+
'notes' => $order->notes,
64+
'processed_by' => $order->processedBy ? ['name' => $order->processedBy->name] : null,
65+
'created_at' => $order->created_at,
66+
'items' => $order->items->map(fn ($item) => [
67+
'id' => $item->id,
68+
'product_name' => $item->product_name,
69+
'product_sku' => $item->product_sku,
70+
'quantity' => $item->quantity,
71+
'unit_price' => $item->unit_price,
72+
'line_total' => $item->line_total,
73+
]),
74+
],
75+
]);
76+
}
77+
78+
public function confirm(StoreOrder $order): RedirectResponse
79+
{
80+
$order->confirm();
81+
82+
return redirect()->back()->with('success', 'Order confirmed.');
83+
}
84+
85+
public function markPaid(StoreOrder $order): RedirectResponse
86+
{
87+
$order->markPaid();
88+
89+
return redirect()->back()->with('success', 'Order marked as paid.');
90+
}
91+
92+
public function ship(StoreOrder $order): RedirectResponse
93+
{
94+
$order->ship();
95+
96+
return redirect()->back()->with('success', 'Order marked as shipped.');
97+
}
98+
99+
public function deliver(StoreOrder $order): RedirectResponse
100+
{
101+
$order->deliver();
102+
103+
return redirect()->back()->with('success', 'Order marked as delivered.');
104+
}
105+
106+
public function cancel(StoreOrder $order): RedirectResponse
107+
{
108+
$order->cancel();
109+
110+
return redirect()->back()->with('success', 'Order cancelled.');
111+
}
112+
}

0 commit comments

Comments
 (0)