Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
50.00% covered (danger)
50.00%
33 / 66
54.55% covered (warning)
54.55%
6 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
OrderController
50.00% covered (danger)
50.00%
33 / 66
54.55% covered (warning)
54.55%
6 / 11
43.12
0.00% covered (danger)
0.00%
0 / 1
 index
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 details
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 changeStatus
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 paymentStatus
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
12
 show
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 edit
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 update
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 updateDetails
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 refundPayment
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 autoPostRefund
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 destroy
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace App\Http\Controllers;
4
5use App\Enums\PaymentStatus;
6use App\Models\Order;
7use App\Models\Payment;
8use App\Service\OrderService;
9use App\Services\Accounting\AccountingService;
10use App\Services\Location\BranchContext;
11use App\Services\Payment\PaymentManager;
12use Illuminate\Http\Request;
13use Illuminate\Http\Response;
14use Illuminate\Support\Facades\DB;
15use Illuminate\Support\Facades\Log;
16use InvalidArgumentException;
17
18class OrderController extends Controller
19{
20    public function index()
21    {
22        $data['data'] = app(BranchContext::class)->scope(Order::query())
23            ->with(['customer', 'payment', 'details.stock.product', 'details.stock.brand', 'details.stock.color', 'details.stock.size'])
24            ->latest()->paginate(15);
25
26        return view('admin.order.index')->with($data);
27    }
28
29    public function details($id)
30    {
31        $orderQuery = app(BranchContext::class)->scope(Order::query());
32        $data['order'] = $orderQuery->findOrFail($id);
33        $data['details'] = OrderService::getDetails($id);
34        // Timeline (Phase 8) — reuses Order's existing Auditable trait, the
35        // same data that backs People → Activity log; no new logging needed.
36        $data['activities'] = $data['order']->activities()->with('causer')->latest()->get();
37
38        return view('admin.order.details')->with($data);
39    }
40
41    public function changeStatus(Request $request)
42    {
43        $order = app(BranchContext::class)->scope(Order::query())->findOrFail($request->id);
44
45        // FIX: this used to write $request->status straight onto the order
46        // with zero validation — any arbitrary string from the client would
47        // be saved as the order's status. OrderService::transition() now
48        // rejects anything that isn't a real App\Enums\OrderStatus value.
49        try {
50            OrderService::transition($order, (string) $request->status);
51        } catch (InvalidArgumentException $e) {
52            return response()->json(['error' => $e->getMessage()], 422);
53        }
54
55        return response()->json(['success', 'Order ' . $order->status . ' Successfully']);
56    }
57
58    public function paymentStatus(Request $request)
59    {
60        $request->validate([
61            'id' => ['required', 'integer', 'exists:orders,id'],
62            'status' => ['required', 'in:Paid'],
63        ]);
64
65        try {
66            DB::beginTransaction();
67            $order = app(BranchContext::class)->scope(Order::query())->lockForUpdate()->findOrFail($request->id);
68            $remaining = max(0, $order->grandTotal() - (float) $order->payment()->sum('paid'));
69            if ($remaining <= 0) {
70                DB::rollBack();
71
72                return response()->json(['error' => 'This order is already fully paid.'], 422);
73            }
74
75            $payment = Payment::query()->create([
76                'order_id' => $request->id,
77                // FIX: was 'cutomer_id' (typo) — didn't match the real
78                // 'customer_id' column or the model's $fillable, so it was
79                // silently dropped and every payment row's customer_id stayed null.
80                'customer_id' => optional($order->customer)->id,
81                'shipping_cost' => 0,
82                'total' => $order->grandTotal(),
83                'paid' => $remaining,
84                'payment_type' => 'Cash on delivery',
85                'payment_note' => 'Marked fully paid',
86                'status' => PaymentStatus::Paid->value,
87                'created_by' => $request->user()?->id,
88            ]);
89
90            // FIX: the "Mark as Paid" button sends a 'status' field (see
91            // paymentStatus() in the sale list / order table JS), but this used
92            // to read $request->payment_status — a key nothing ever sent — so
93            // $order->payment_status was being set to null on every use.
94            $order->update(['payment_status' => 'Paid', 'due' => 0]);
95            app(AccountingService::class)->autoPostOrderPayment($order, $payment);
96            DB::commit();
97
98            return response()->json(['success' => 'Order payment status updated successfully']);
99        } catch (\Throwable $e) {
100            DB::rollBack();
101            Log::error('OrderController::paymentStatus failed: ' . $e->getMessage());
102
103            return response()->json(['error' => 'Could not update the payment status. Please try again.'], 422);
104        }
105    }
106
107    public function show(Order $order)
108    {
109        //
110    }
111
112    public function edit(Order $order)
113    {
114        //
115    }
116
117    /**
118     * Update the specified resource in storage.
119     *
120     * @return Response
121     */
122    public function update(Request $request, Order $order)
123    {
124        //
125    }
126
127    /**
128     * Admin Order workspace (Phase 8) — internal note + courier/tracking,
129     * saved together from one card. Every change is already audit-logged
130     * via Order's Auditable trait, which is what feeds the page's timeline.
131     */
132    public function updateDetails(Request $request, Order $order)
133    {
134        app(BranchContext::class)->ensureBranch($order->branch_id);
135        $data = $request->validate([
136            'note' => 'nullable|string|max:2000',
137            'courier_name' => 'nullable|string|max:120',
138            'tracking_number' => 'nullable|string|max:120',
139        ]);
140
141        $order->update($data);
142
143        return redirect()->route('admin.order.details', $order->id)->with('success', 'Order details updated.');
144    }
145
146    /**
147     * Refund a recorded payment through whichever gateway processed it
148     * (App\Services\Payment\PaymentManager) — false (never an exception)
149     * when that gateway/payment doesn't support it, so the admin sees
150     * "not supported" instead of a crash. Never refunds more than what was
151     * actually paid.
152     */
153    public function refundPayment(Request $request, Payment $payment, PaymentManager $paymentManager)
154    {
155        app(BranchContext::class)->ensureBranch($payment->order?->branch_id);
156        $data = $request->validate([
157            'amount' => 'required|numeric|min:0.01|max:' . (float) $payment->paid,
158        ]);
159
160        if (! $paymentManager->refund($payment, (float) $data['amount'])) {
161            return back()->with('error', 'This payment cannot be refunded automatically — refund it with the provider directly, then note it on the order.');
162        }
163
164        $payment->status = PaymentStatus::Refunded->value;
165        $payment->save();
166
167        $this->autoPostRefund($payment, (float) $data['amount']);
168
169        return back()->with('success', 'Refund processed.');
170    }
171
172    private function autoPostRefund(Payment $payment, float $amount): void
173    {
174        app(AccountingService::class)->autoPostRefund($payment, $amount);
175    }
176
177    /**
178     * Remove the specified resource from storage.
179     *
180     * @return Response
181     */
182    public function destroy(Order $order)
183    {
184        //
185    }
186}