TA/web_JMW/app/Http/Controllers/TransactionController.php

317 lines
12 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Category;
use App\Models\Transaction;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
class TransactionController extends Controller
{
public function index()
{
Category::releaseExpiredReservations();
$transactions = Transaction::with('category')->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;
}
}