Skip to content

Commit bdfd1e4

Browse files
committed
Phases 176-180: Point of Sale Module + Finance table rename fix — 1658 tests
POS: 4 migrations (pos_sessions, pos_orders, pos_order_items, pos_payments), 4 models (PosSession/Order/OrderItem/Payment with POS-YYYY-NNNNN / REC-YYYY-NNNNN numbering), PosPolicy, 3 controllers (Session/Order/Dashboard), POSServiceProvider, 7 React pages (Dashboard, Sessions CRUD+ZReport, Orders Index+Receipt), 17 tests. Fix: renamed Finance projects tables to finance_projects/finance_project_tasks/ finance_project_time_entries to avoid collision with PM module's projects table. 1658/1658 feature tests passing. https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent f4c5cd9 commit bdfd1e4

27 files changed

Lines changed: 2210 additions & 2 deletions

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use App\Modules\Manufacturing\Providers\ManufacturingServiceProvider;
1313
use App\Modules\CRM\Providers\CRMServiceProvider;
1414
use App\Modules\PM\Providers\PMServiceProvider;
15+
use App\Modules\POS\Providers\POSServiceProvider;
1516
use Illuminate\Support\Facades\Gate;
1617
use Illuminate\Support\ServiceProvider;
1718

@@ -25,6 +26,7 @@ public function register(): void
2526
$this->app->register(ManufacturingServiceProvider::class);
2627
$this->app->register(CRMServiceProvider::class);
2728
$this->app->register(PMServiceProvider::class);
29+
$this->app->register(POSServiceProvider::class);
2830
}
2931

3032
public function boot(): void

erp/app/Modules/Finance/Models/Project.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ class Project extends Model
1313
use BelongsToTenant;
1414
use SoftDeletes;
1515

16+
protected $table = 'finance_projects';
17+
1618
protected $fillable = [
1719
'tenant_id',
1820
'name',

erp/app/Modules/Finance/Models/ProjectTask.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class ProjectTask extends Model
1111
{
1212
use BelongsToTenant;
1313

14-
protected $table = 'project_tasks';
14+
protected $table = 'finance_project_tasks';
1515

1616
protected $fillable = [
1717
'tenant_id',

erp/app/Modules/Finance/Models/ProjectTimeEntry.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class ProjectTimeEntry extends Model
1111
{
1212
use BelongsToTenant;
1313

14-
protected $table = 'project_time_entries';
14+
protected $table = 'finance_project_time_entries';
1515

1616
protected $fillable = [
1717
'tenant_id',
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace App\Modules\POS\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\POS\Models\PosOrder;
7+
use App\Modules\POS\Models\PosSession;
8+
use Inertia\Inertia;
9+
use Inertia\Response;
10+
11+
class PosDashboardController extends Controller
12+
{
13+
public function index(): Response
14+
{
15+
$openSessions = PosSession::where('status', 'open')->count();
16+
17+
$todaySales = PosOrder::whereDate('created_at', today())
18+
->where('status', 'completed')
19+
->sum('total');
20+
21+
$todayOrderCount = PosOrder::whereDate('created_at', today())
22+
->where('status', 'completed')
23+
->count();
24+
25+
$avgOrderValue = $todayOrderCount > 0
26+
? round($todaySales / $todayOrderCount, 2)
27+
: 0;
28+
29+
$recentOrders = PosOrder::with(['session', 'servedBy'])
30+
->latest()
31+
->take(10)
32+
->get()
33+
->map(fn ($o) => [
34+
'id' => $o->id,
35+
'receipt_number' => $o->receipt_number,
36+
'customer_name' => $o->customer_name,
37+
'total' => $o->total,
38+
'payment_method' => $o->payment_method,
39+
'status' => $o->status,
40+
'created_at' => $o->created_at,
41+
]);
42+
43+
return Inertia::render('POS/Dashboard', [
44+
'stats' => [
45+
'open_sessions' => $openSessions,
46+
'today_sales' => $todaySales,
47+
'today_orders' => $todayOrderCount,
48+
'avg_order_value' => $avgOrderValue,
49+
],
50+
'recentOrders' => $recentOrders,
51+
]);
52+
}
53+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<?php
2+
3+
namespace App\Modules\POS\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\POS\Models\PosOrder;
7+
use App\Modules\POS\Models\PosOrderItem;
8+
use App\Modules\POS\Models\PosSession;
9+
use Illuminate\Http\Request;
10+
use Inertia\Inertia;
11+
use Inertia\Response;
12+
13+
class PosOrderController extends Controller
14+
{
15+
public function index(Request $request): Response
16+
{
17+
$orders = PosOrder::with(['session'])
18+
->when($request->status, fn ($q) => $q->where('status', $request->status))
19+
->when($request->session_id, fn ($q) => $q->where('session_id', $request->session_id))
20+
->latest()
21+
->paginate(25)
22+
->through(fn ($o) => [
23+
'id' => $o->id,
24+
'receipt_number' => $o->receipt_number,
25+
'customer_name' => $o->customer_name,
26+
'session' => $o->session ? ['id' => $o->session->id, 'name' => $o->session->name] : null,
27+
'total' => $o->total,
28+
'payment_method' => $o->payment_method,
29+
'status' => $o->status,
30+
'created_at' => $o->created_at,
31+
]);
32+
33+
return Inertia::render('POS/Orders/Index', [
34+
'orders' => $orders,
35+
'filters' => $request->only(['status', 'session_id']),
36+
]);
37+
}
38+
39+
public function store(Request $request): Response
40+
{
41+
$data = $request->validate([
42+
'session_id' => 'required|exists:pos_sessions,id',
43+
'customer_name' => 'nullable|string|max:255',
44+
'customer_email' => 'nullable|email|max:255',
45+
'discount_amount' => 'nullable|numeric|min:0',
46+
'tax_amount' => 'nullable|numeric|min:0',
47+
'amount_paid' => 'required|numeric|min:0',
48+
'payment_method' => 'required|in:cash,card,digital_wallet,split',
49+
'notes' => 'nullable|string',
50+
'items' => 'required|array|min:1',
51+
'items.*.product_id' => 'nullable|exists:products,id',
52+
'items.*.product_name' => 'required|string',
53+
'items.*.product_sku' => 'nullable|string',
54+
'items.*.quantity' => 'required|numeric|min:0.001',
55+
'items.*.unit_price' => 'required|numeric|min:0',
56+
'items.*.discount_percent' => 'nullable|numeric|min:0|max:100',
57+
'items.*.line_total' => 'required|numeric|min:0',
58+
]);
59+
60+
$subtotal = collect($data['items'])->sum('line_total');
61+
$discountAmount = $data['discount_amount'] ?? 0;
62+
$taxAmount = $data['tax_amount'] ?? 0;
63+
$total = $subtotal - $discountAmount + $taxAmount;
64+
$amountPaid = $data['amount_paid'];
65+
$changeGiven = max(0, $amountPaid - $total);
66+
67+
$order = PosOrder::create([
68+
'tenant_id' => auth()->user()->tenant_id,
69+
'session_id' => $data['session_id'],
70+
'customer_name' => $data['customer_name'] ?? null,
71+
'customer_email' => $data['customer_email'] ?? null,
72+
'subtotal' => $subtotal,
73+
'discount_amount' => $discountAmount,
74+
'tax_amount' => $taxAmount,
75+
'total' => $total,
76+
'amount_paid' => $amountPaid,
77+
'change_given' => $changeGiven,
78+
'payment_method' => $data['payment_method'],
79+
'status' => 'completed',
80+
'notes' => $data['notes'] ?? null,
81+
'served_by' => auth()->id(),
82+
'created_by' => auth()->id(),
83+
]);
84+
85+
$order->receipt_number = $order->generateReceiptNumber();
86+
$order->save();
87+
88+
foreach ($data['items'] as $item) {
89+
PosOrderItem::create([
90+
'order_id' => $order->id,
91+
'product_id' => $item['product_id'] ?? null,
92+
'product_name' => $item['product_name'],
93+
'product_sku' => $item['product_sku'] ?? null,
94+
'quantity' => $item['quantity'],
95+
'unit_price' => $item['unit_price'],
96+
'discount_percent' => $item['discount_percent'] ?? 0,
97+
'line_total' => $item['line_total'],
98+
]);
99+
}
100+
101+
// Update session total_sales
102+
$session = PosSession::find($data['session_id']);
103+
if ($session) {
104+
$session->total_sales += $total;
105+
$session->save();
106+
}
107+
108+
$order->load('items', 'session', 'servedBy');
109+
110+
return Inertia::render('POS/Orders/Receipt', [
111+
'order' => [
112+
'id' => $order->id,
113+
'receipt_number' => $order->receipt_number,
114+
'customer_name' => $order->customer_name,
115+
'customer_email' => $order->customer_email,
116+
'subtotal' => $order->subtotal,
117+
'discount_amount'=> $order->discount_amount,
118+
'tax_amount' => $order->tax_amount,
119+
'total' => $order->total,
120+
'amount_paid' => $order->amount_paid,
121+
'change_given' => $order->change_given,
122+
'payment_method' => $order->payment_method,
123+
'status' => $order->status,
124+
'created_at' => $order->created_at,
125+
'session' => $order->session ? ['id' => $order->session->id, 'name' => $order->session->name] : null,
126+
'items' => $order->items->map(fn ($i) => [
127+
'id' => $i->id,
128+
'product_name' => $i->product_name,
129+
'product_sku' => $i->product_sku,
130+
'quantity' => $i->quantity,
131+
'unit_price' => $i->unit_price,
132+
'discount_percent' => $i->discount_percent,
133+
'line_total' => $i->line_total,
134+
]),
135+
],
136+
]);
137+
}
138+
139+
public function show(PosOrder $order): Response
140+
{
141+
$order->load(['items', 'session', 'servedBy']);
142+
143+
return Inertia::render('POS/Orders/Receipt', [
144+
'order' => [
145+
'id' => $order->id,
146+
'receipt_number' => $order->receipt_number,
147+
'customer_name' => $order->customer_name,
148+
'customer_email' => $order->customer_email,
149+
'subtotal' => $order->subtotal,
150+
'discount_amount'=> $order->discount_amount,
151+
'tax_amount' => $order->tax_amount,
152+
'total' => $order->total,
153+
'amount_paid' => $order->amount_paid,
154+
'change_given' => $order->change_given,
155+
'payment_method' => $order->payment_method,
156+
'status' => $order->status,
157+
'created_at' => $order->created_at,
158+
'session' => $order->session ? ['id' => $order->session->id, 'name' => $order->session->name] : null,
159+
'items' => $order->items->map(fn ($i) => [
160+
'id' => $i->id,
161+
'product_name' => $i->product_name,
162+
'product_sku' => $i->product_sku,
163+
'quantity' => $i->quantity,
164+
'unit_price' => $i->unit_price,
165+
'discount_percent' => $i->discount_percent,
166+
'line_total' => $i->line_total,
167+
]),
168+
],
169+
]);
170+
}
171+
172+
public function refund(PosOrder $order): \Illuminate\Http\RedirectResponse
173+
{
174+
$order->status = 'refunded';
175+
$order->save();
176+
177+
$session = $order->session;
178+
if ($session) {
179+
$session->total_refunds += $order->total;
180+
$session->save();
181+
}
182+
183+
return redirect()->back()->with('success', 'Order refunded successfully.');
184+
}
185+
}

0 commit comments

Comments
 (0)