Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.11% covered (warning)
86.11%
31 / 36
0.00% covered (danger)
0.00%
0 / 1
CRAP
0.00% covered (danger)
0.00%
0 / 1
PaymentController
86.11% covered (warning)
86.11%
31 / 36
0.00% covered (danger)
0.00%
0 / 1
5.07
0.00% covered (danger)
0.00%
0 / 1
 duePayment
86.11% covered (warning)
86.11%
31 / 36
0.00% covered (danger)
0.00%
0 / 1
5.07
1<?php
2
3namespace App\Http\Controllers\admin;
4
5use App\Http\Controllers\Controller;
6use App\Models\Order;
7use App\Models\Payment;
8use App\Models\PaymentType;
9use App\Services\Accounting\AccountingService;
10use App\Services\Location\BranchContext;
11use Illuminate\Http\Request;
12use Illuminate\Support\Facades\DB;
13use Illuminate\Support\Facades\Log;
14
15class PaymentController extends Controller
16{
17    public function duePayment(Request $request)
18    {
19        $data = $request->validate([
20            'order_id' => ['required', 'integer', 'exists:orders,id'],
21            'payment_type_id' => ['required', 'integer', 'exists:payment_types,id'],
22            'paid' => ['required', 'numeric', 'min:0.01'],
23            'deliver_date' => ['nullable', 'date'],
24            'note' => ['nullable', 'string', 'max:500'],
25        ]);
26
27        try {
28            DB::beginTransaction();
29            $order = app(BranchContext::class)->scope(Order::query())->lockForUpdate()->findOrFail($data['order_id']);
30            $due = max(0, $order->grandTotal() - (float) $order->payment()->sum('paid'));
31            if ($due <= 0 || (float) $data['paid'] > $due) {
32                DB::rollBack();
33
34                return back()->withErrors(['paid' => 'Payment must not exceed the current due amount.'])->withInput();
35            }
36
37            // FIX: payment_type / payment_note used to be hardcoded ('Cash on
38            // delivery' / 'Payment pending') and the values actually typed into
39            // the Custom Payment form (payment_type_id, note) were silently
40            // discarded — Payment::$fillable didn't even include those two
41            // columns, so they were dropped by mass-assignment regardless.
42            $paymentTypeName = optional(PaymentType::find($data['payment_type_id']))->name
43                ?? 'Cash on delivery';
44
45            $pay = Payment::create([
46                'order_id' => $order->id,
47                'customer_id' => optional($order->customer)->id,
48                'paid' => $data['paid'],
49                'payment_type' => $paymentTypeName,
50                'payment_note' => $data['note'] ?? null,
51                'status' => 'Paid',
52                'created_by' => $request->user()?->id,
53            ]);
54
55            // FIX: was comparing against $order->total only, so a due payment
56            // that fully covered total + shipping_cost still left the order
57            // marked as "Due" — same shipping_cost-excluded bug as the main
58            // order table's Due column used to have before the table redesign.
59            $remaining = max(0, $order->grandTotal() - (float) $order->payment()->sum('paid'));
60            $order->update([
61                'due' => $remaining,
62                'payment_status' => $remaining <= 0 ? 'Paid' : 'Partial',
63            ]);
64
65            app(AccountingService::class)->autoPostOrderPayment($order, $pay);
66            DB::commit();
67
68            return redirect()->back()->with('success', 'Payment successfully recorded.');
69        } catch (\Throwable $e) {
70            // FIX: this used to call DB::commit() on failure instead of
71            // DB::rollBack(), and had no return — a failed payment could commit
72            // a half-finished write and then respond with nothing at all.
73            DB::rollBack();
74            Log::error('PaymentController::duePayment failed: ' . $e->getMessage());
75
76            return redirect()->back()->with('error', 'Could not record this payment. Please try again.');
77        }
78    }
79}