Bagian 5 dari seri
Membuat Transaksi Penjualan Sistem Kasir Laravel
Diterbitkan 15 September 2026
Buat controller:
php artisan make:controller SaleController
Route:
Route::middleware('auth')->group(function () {
Route::get('sales', [SaleController::class, 'index'])->name('sales.index');
Route::get('sales/create', [SaleController::class, 'create'])->name('sales.create');
Route::post('sales', [SaleController::class, 'store'])->name('sales.store');
Route::get('sales/{sale}', [SaleController::class, 'show'])->name('sales.show');
Route::post('sales/{sale}/cancel', [SaleController::class, 'cancel'])->name('sales.cancel');
});
Validasi item transaksi:
$data = $request->validate([
'customer_id' => ['nullable', 'exists:customers,id'],
'paid_amount' => ['required', 'numeric', 'min:0'],
'discount' => ['nullable', 'numeric', 'min:0'],
'notes' => ['nullable', 'string', 'max:1000'],
'items' => ['required', 'array', 'min:1'],
'items.*.product_id' => ['required', 'distinct', 'exists:products,id'],
'items.*.quantity' => ['required', 'integer', 'min:1'],
]);
Simpan memakai transaksi database dan kunci baris stok:
$sale = DB::transaction(function () use ($data) {
$sale = Sale::create([
'invoice_number' => 'INV-'.now()->format('YmdHis').'-'.random_int(100, 999),
'user_id' => auth()->id(),
'customer_id' => $data['customer_id'] ?? null,
'sale_date' => now(), 'subtotal' => 0, 'discount' => $data['discount'] ?? 0,
'grand_total' => 0, 'paid_amount' => $data['paid_amount'],
'change_amount' => 0, 'status' => 'paid', 'notes' => $data['notes'] ?? null,
]);
$subtotal = 0;
foreach ($data['items'] as $item) {
$product = Product::lockForUpdate()->findOrFail($item['product_id']);
if (!$product->is_active || $product->stock < $item['quantity']) {
throw ValidationException::withMessages(['items' => "Stok {$product->name} tidak mencukupi."]);
}
$lineTotal = $product->selling_price * $item['quantity'];
$sale->details()->create([
'product_id' => $product->id, 'quantity' => $item['quantity'],
'price' => $product->selling_price, 'discount' => 0, 'subtotal' => $lineTotal,
]);
$product->decrement('stock', $item['quantity']);
$product->stockMovements()->create([
'type' => 'sale', 'quantity_out' => $item['quantity'],
'reference_type' => Sale::class, 'reference_id' => $sale->id,
]);
$subtotal += $lineTotal;
}
$total = max(0, $subtotal - $sale->discount);
if ($sale->paid_amount < $total) {
throw ValidationException::withMessages(['paid_amount' => 'Pembayaran belum mencukupi.']);
}
$sale->update(['subtotal' => $subtotal, 'grand_total' => $total, 'change_amount' => $sale->paid_amount - $total]);
return $sale;
});
Harga harus selalu dibaca kembali dari database. Jangan mempercayai harga atau total yang dikirim browser.
Halaman kasir berisi pencarian SKU, keranjang, jumlah, subtotal, pembayaran, dan kembalian. Setelah berhasil, arahkan ke sales.show untuk mencetak nota.




