latest()->get(); $summary = [ 'total' => $transactions->count(), 'paid' => $transactions->where('payment_status', 'paid')->count(), 'sales' => $transactions->where('payment_status', 'paid')->sum('total_amount'), 'today' => $transactions->where('created_at', '>=', today())->count(), ]; return view('admin.riwayat', compact('transactions', 'summary')); } public function create() { Category::releaseExpiredReservations(); $categories = Category::where('status', 'available')->orderBy('name')->get(); $transactionCode = $this->generateTransactionCode(); return view('admin.input', compact('categories', 'transactionCode')); } public function store(Request $request) { Category::releaseExpiredReservations(); $validated = $request->validate([ 'customer_name' => 'required|string|max:255', 'customer_phone' => 'nullable|string|max:30', 'customer_email' => 'nullable|email|max:255', 'transaction_code' => 'nullable|string|max:30|unique:transactions,transaction_code', 'category_id' => 'required|exists:categories,id', 'quantity' => 'required|integer|min:1', 'payment_method' => 'required|in:cash,transfer', 'payment_status' => 'required|in:pending,paid,cancelled', 'payment_proof' => 'nullable|file|mimes:jpg,jpeg,png,pdf|max:2048', 'note' => 'nullable|string|max:1000', ], [ 'customer_name.required' => 'Nama customer wajib diisi.', 'category_id.required' => 'Produk wajib dipilih.', 'category_id.exists' => 'Produk yang dipilih tidak tersedia.', 'quantity.required' => 'Jumlah wajib diisi.', 'quantity.integer' => 'Jumlah harus berupa angka.', 'quantity.min' => 'Jumlah minimal 1.', 'payment_method.required' => 'Metode pembayaran wajib dipilih.', 'payment_status.required' => 'Status pembayaran wajib dipilih.', 'customer_email.email' => 'Format email tidak valid.', 'payment_proof.mimes' => 'Bukti pembayaran harus berupa JPG, PNG, atau PDF.', 'payment_proof.max' => 'Ukuran bukti pembayaran maksimal 2 MB.', ]); $category = Category::findOrFail($validated['category_id']); if ($category->status !== 'available') { return back() ->withErrors(['category_id' => 'Mobil ini sedang tidak tersedia dan tidak bisa dibuatkan transaksi baru.']) ->withInput(); } $quantity = (int) $validated['quantity']; $unitPrice = $category->price; $paymentProofPath = $request->hasFile('payment_proof') ? $request->file('payment_proof')->store('payment-proofs', 'public') : null; $expiresAt = $validated['payment_status'] === 'pending' ? now()->addMinutes(Category::RESERVATION_MINUTES) : null; Transaction::create($this->transactionPayload([ 'transaction_code' => $validated['transaction_code'] ?? $this->generateTransactionCode(), 'category_id' => $category->id, 'admin_username' => auth('admin')->user()->username ?? null, 'customer_name' => $validated['customer_name'], 'customer_phone' => $validated['customer_phone'] ?? null, 'customer_email' => $validated['customer_email'] ?? null, 'quantity' => $quantity, 'unit_price' => $unitPrice, 'total_amount' => $unitPrice * $quantity, 'payment_method' => $validated['payment_method'], 'payment_status' => $validated['payment_status'], 'expires_at' => $expiresAt, 'reserved_by' => $validated['payment_status'] === 'pending' ? (auth('admin')->user()->username ?? 'admin') : null, 'payment_proof' => $paymentProofPath, 'note' => $validated['note'] ?? null, ])); if ($validated['payment_status'] === 'paid') { $category->update([ 'status' => 'sold', ]); } elseif ($validated['payment_status'] === 'pending') { $category->update([ 'status' => 'reserved', ]); } return redirect()->route('riwayat')->with('success', 'Transaksi berhasil disimpan.'); } public function beginPayment(Category $category) { Category::releaseExpiredReservations(); $transaction = DB::transaction(function () use ($category) { $lockedCategory = Category::whereKey($category->id)->lockForUpdate()->firstOrFail(); if ($lockedCategory->status !== 'available') { return null; } $expiresAt = now()->addMinutes(Category::RESERVATION_MINUTES); $user = auth()->user(); $transaction = Transaction::create($this->transactionPayload([ 'transaction_code' => $this->generateTransactionCode(), 'category_id' => $lockedCategory->id, 'customer_name' => $user->username, 'customer_phone' => $user->phone_number, 'customer_email' => $user->email, 'quantity' => 1, 'unit_price' => $lockedCategory->price, 'total_amount' => $lockedCategory->price, 'payment_method' => 'transfer', 'payment_status' => 'pending', 'expires_at' => $expiresAt, 'reserved_by' => $user->username, ])); $lockedCategory->update([ 'status' => 'reserved', ]); return $transaction; }); if (! $transaction) { return redirect()->route('mobil-bekas.detail', $category) ->with('error', 'Maaf, mobil ini baru saja dipesan pengguna lain atau sudah terjual.'); } return redirect()->route('transactions.payment', $transaction); } public function showPayment(Transaction $transaction) { Category::releaseExpiredReservations(); $transaction->refresh(); if ($transaction->payment_status === 'cancelled' || ! $transaction->category) { return redirect()->route('semua-mobil-bekas') ->with('error', 'Waktu pembayaran telah habis. Mobil kembali tersedia.'); } if (! $this->isTransactionOwner($transaction)) { abort(403, 'Anda tidak memiliki akses ke pembayaran ini.'); } return view('user.payment', compact('transaction')); } public function uploadProof(Request $request, Transaction $transaction) { Category::releaseExpiredReservations(); $transaction->refresh(); if ($transaction->payment_status !== 'pending' || ! $transaction->expires_at || $transaction->expires_at->isPast()) { Category::releaseExpiredReservations(); return redirect()->route('semua-mobil-bekas') ->with('error', 'Waktu pembayaran telah habis. Mobil kembali tersedia.'); } if (! $this->isTransactionOwner($transaction)) { abort(403, 'Anda tidak memiliki akses ke pembayaran ini.'); } $validated = $request->validate([ 'customer_name' => 'required|string|max:255', 'customer_phone' => 'nullable|string|max:30', 'customer_email' => 'nullable|email|max:255', 'payment_method' => 'required|in:cash,transfer', 'payment_proof' => 'required|file|mimes:jpg,jpeg,png,pdf|max:2048', 'note' => 'nullable|string|max:1000', ], [ 'customer_name.required' => 'Nama pembeli wajib diisi.', 'payment_method.required' => 'Metode pembayaran wajib dipilih.', 'payment_proof.required' => 'Bukti pembayaran wajib diupload.', 'payment_proof.mimes' => 'Bukti pembayaran harus berupa JPG, PNG, atau PDF.', 'payment_proof.max' => 'Ukuran bukti pembayaran maksimal 2 MB.', 'customer_email.email' => 'Format email tidak valid.', ]); $paymentProofPath = $request->file('payment_proof')->store('payment-proofs', 'public'); $updated = DB::transaction(function () use ($transaction, $validated, $paymentProofPath) { $lockedTransaction = Transaction::whereKey($transaction->id)->lockForUpdate()->firstOrFail(); $lockedCategory = $lockedTransaction->category()->lockForUpdate()->first(); if (! $lockedCategory || $lockedTransaction->payment_status !== 'pending' || ! $lockedTransaction->expires_at || $lockedTransaction->expires_at->isPast() || $lockedTransaction->reserved_by !== auth()->user()->username) { return false; } $lockedTransaction->update([ 'customer_name' => $validated['customer_name'], 'customer_phone' => $validated['customer_phone'] ?? null, 'customer_email' => $validated['customer_email'] ?? null, 'payment_method' => $validated['payment_method'], 'payment_proof' => $paymentProofPath, 'note' => $validated['note'] ?? null, ]); return true; }); if (! $updated) { Storage::disk('public')->delete($paymentProofPath); Category::releaseExpiredReservations(); return redirect()->route('semua-mobil-bekas')->with('error', 'Waktu pembayaran telah habis. Mobil kembali tersedia.'); } return redirect()->route('mobil-bekas.detail', $transaction->category_id) ->with('success', 'Bukti pembayaran berhasil dikirim dan sedang menunggu konfirmasi admin.'); } public function confirmPayment(Transaction $transaction) { Category::releaseExpiredReservations(); $transaction->refresh(); if ($transaction->payment_status === 'cancelled') { return back()->with('error', 'Transaksi ini sudah dibatalkan karena melewati batas waktu pembayaran.'); } $confirmed = DB::transaction(function () use ($transaction) { $lockedTransaction = Transaction::whereKey($transaction->id)->lockForUpdate()->firstOrFail(); $lockedCategory = $lockedTransaction->category() ->lockForUpdate() ->first(); if (! $lockedCategory || $lockedCategory->status === 'sold') { return false; } $lockedTransaction->update($this->transactionPayload([ 'payment_status' => 'paid', 'admin_username' => auth('admin')->user()->username ?? null, 'expires_at' => null, ])); $lockedCategory->update([ 'status' => 'sold', ]); return true; }); if (! $confirmed) { return back()->with('error', 'Mobil ini sudah terjual, transaksi pending lain tidak bisa dikonfirmasi.'); } return redirect()->route('riwayat')->with('success', 'Pembayaran dikonfirmasi dan mobil ditandai terjual.'); } private function generateTransactionCode() { do { $code = 'TRX' . now()->format('YmdHis') . random_int(10, 99); } while (Transaction::where('transaction_code', $code)->exists()); return $code; } private function transactionPayload(array $payload): array { if (! Schema::hasColumn('transactions', 'expires_at')) { unset($payload['expires_at']); } if (! Schema::hasColumn('transactions', 'reserved_by')) { unset($payload['reserved_by']); } return $payload; } private function isTransactionOwner(Transaction $transaction): bool { return auth()->check() && $transaction->reserved_by === auth()->user()->username; } }