317 lines
12 KiB
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;
|
|
}
|
|
}
|