Membuat Struktur Database Sistem Kasir Laravel
Diterbitkan 15 September 2026
Pada tahap kedua kita akan membuat struktur database Sistem Kasir Laravel menggunakan migration Laravel.
Pada tahap sebelumnya aplikasi sudah memiliki login, role administrator dan kasir, middleware akses, serta tabel users. Sekarang kita akan menambahkan seluruh tabel bisnis yang dibutuhkan untuk menyimpan data kategori, satuan, produk, pelanggan, supplier, pembelian, penjualan, detail transaksi, riwayat pergerakan stok, dan pengaturan aplikasi.
Database dibuat menggunakan migration agar struktur tabel tidak perlu dibuat secara manual melalui phpMyAdmin. Migration juga membuat struktur database lebih mudah dipindahkan ke komputer lain atau server hosting karena seluruh definisi tabel tersimpan di dalam project Laravel.
Setelah menyelesaikan tahap ini, database sistem kasir sudah siap digunakan pada tahap berikutnya untuk membuat model, relasi Eloquent, dan data awal.
Hasil yang Akan Dibuat
Pada tahap ini kita akan membuat tabel:
userssebagai pengguna sistem yang sudah tersedia dari tahap sebelumnya;categoriesuntuk kategori produk;unitsuntuk satuan produk;productsuntuk data barang;customersuntuk data pelanggan;suppliersuntuk data pemasok;salesuntuk header transaksi penjualan;sale_detailsuntuk item penjualan;purchasesuntuk header transaksi pembelian;purchase_detailsuntuk item pembelian;stock_movementsuntuk riwayat stok masuk dan keluar;settingsuntuk pengaturan aplikasi.
Pada akhir tahap ini seluruh migration harus dapat dijalankan tanpa error menggunakan:
php artisan migrate
Persiapan
Pastikan Tahap 01 sudah selesai dan project dapat dijalankan.
Masuk ke folder project:
cd kasir-laravel
Pastikan konfigurasi database pada file .env masih menggunakan database:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=kasir_laravel
DB_USERNAME=root
DB_PASSWORD=
Sesuaikan DB_USERNAME dan DB_PASSWORD apabila konfigurasi MySQL pada komputer berbeda.
Untuk memastikan Laravel dapat membaca konfigurasi terbaru, jalankan:
php artisan config:clear
1. Memahami Struktur Database Sistem Kasir
Sebelum membuat migration, pahami terlebih dahulu kelompok tabel yang akan digunakan.
Secara sederhana database dibagi menjadi empat kelompok.
Tabel pengguna
Tabel:
users
Tabel ini sudah tersedia dari Tahap 01 dan digunakan untuk akun administrator serta kasir.
Tabel master
Tabel master terdiri dari:
categories
units
products
customers
suppliers
Data master digunakan berulang kali pada transaksi.
Contohnya:
- satu kategori dapat digunakan oleh banyak produk;
- satu satuan dapat digunakan oleh banyak produk;
- satu pelanggan dapat memiliki banyak transaksi penjualan;
- satu supplier dapat memiliki banyak transaksi pembelian.
Tabel transaksi
Tabel transaksi terdiri dari:
sales
sale_details
purchases
purchase_details
Tabel sales menyimpan informasi utama transaksi penjualan.
Tabel sale_details menyimpan produk-produk yang terdapat pada transaksi tersebut.
Konsep yang sama digunakan untuk:
purchases
purchase_details
Dengan pemisahan header dan detail, satu transaksi dapat memiliki banyak item produk.
Tabel pendukung
Tabel pendukung terdiri dari:
stock_movements
settings
stock_movements mencatat riwayat perubahan stok.
settings menyimpan konfigurasi sederhana seperti nama toko, alamat, nomor telepon, atau pesan pada nota.
2. Gambaran Relasi Tabel
Relasi utama database sistem kasir adalah:
users
├── sales
└── purchases
categories
└── products
units
└── products
customers
└── sales
suppliers
└── purchases
sales
└── sale_details
└── products
purchases
└── purchase_details
└── products
products
└── stock_movements
Satu sale dapat memiliki banyak sale_details.
Satu purchase dapat memiliki banyak purchase_details.
Satu product dapat muncul pada banyak transaksi penjualan dan pembelian.
3. Mengapa Menggunakan Migration Laravel
Migration dapat dianggap sebagai version control untuk struktur database.
Dengan migration kita dapat:
- membuat tabel melalui kode PHP;
- menentukan tipe kolom;
- membuat primary key;
- membuat foreign key;
- menentukan aturan ketika data induk dihapus;
- menjalankan struktur database pada komputer lain;
- melakukan rollback saat pengembangan;
- menyimpan perubahan struktur database bersama source code.
File migration disimpan pada folder:
database/migrations
Nama file migration memiliki timestamp di bagian depan. Timestamp tersebut digunakan Laravel untuk menentukan urutan migration.
Karena terdapat banyak foreign key pada sistem kasir, urutan migration sangat penting.
Tabel yang menjadi induk harus dibuat lebih dahulu sebelum tabel yang menggunakannya sebagai foreign key.
4. Urutan Tabel yang Akan Dibuat
Gunakan urutan berikut:
1. users -> sudah tersedia
2. categories
3. units
4. products
5. customers
6. suppliers
7. purchases
8. purchase_details
9. sales
10. sale_details
11. stock_movements
12. settings
Urutan detail dapat sedikit berbeda selama tabel induk selalu dibuat sebelum tabel yang memiliki foreign key kepadanya.
Contohnya:
products
harus dibuat setelah:
categories
units
Karena tabel products mempunyai:
category_id
unit_id
Sedangkan sale_details harus dibuat setelah:
sales
products
5. Membuat Model dan Migration
Kita akan membuat model sekaligus migration menggunakan opsi -m.
Jalankan perintah berikut satu per satu:
php artisan make:model Category -m
php artisan make:model Unit -m
php artisan make:model Product -m
php artisan make:model Customer -m
php artisan make:model Supplier -m
php artisan make:model Purchase -m
php artisan make:model PurchaseDetail -m
php artisan make:model Sale -m
php artisan make:model SaleDetail -m
php artisan make:model StockMovement -m
php artisan make:model Setting -m
Setelah perintah selesai, model akan dibuat pada:
app/Models
Sedangkan migration akan dibuat pada:
database/migrations
Pada tahap ini kita fokus terlebih dahulu pada migration.
Isi model akan dibahas lengkap pada Tahap 03.
6. Migration Categories
Cari migration dengan nama seperti:
xxxx_xx_xx_xxxxxx_create_categories_table.php
Ganti isinya menjadi:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('categories');
}
};
Tabel kategori memiliki kolom:
id
name
created_at
updated_at
Kolom:
$table->string('name')->unique();
berarti nama kategori tidak boleh sama.
Contohnya:
Makanan
Minuman
Sembako
Elektronik
Jika mencoba memasukkan kategori Makanan dua kali, database akan menolak data duplikat.
7. Migration Units
Cari migration:
xxxx_xx_xx_xxxxxx_create_units_table.php
Isi dengan:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('units', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('symbol', 20);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('units');
}
};
Contoh data satuan:
| name | symbol |
|---|---|
| Pieces | pcs |
| Botol | btl |
| Kotak | box |
| Kilogram | kg |
Kolom symbol dibatasi maksimal 20 karakter karena isinya hanya simbol singkat.
8. Migration Products
Cari migration:
xxxx_xx_xx_xxxxxx_create_products_table.php
Isi dengan:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')
->nullable()
->constrained()
->nullOnDelete();
$table->foreignId('unit_id')
->nullable()
->constrained()
->nullOnDelete();
$table->string('sku', 50)->unique();
$table->string('name');
$table->decimal('purchase_price', 15, 2)->default(0);
$table->decimal('selling_price', 15, 2)->default(0);
$table->integer('stock')->default(0);
$table->integer('minimum_stock')->default(0);
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->index('name');
$table->index(['is_active', 'stock']);
});
}
public function down(): void
{
Schema::dropIfExists('products');
}
};
Penjelasan category_id
Kode:
$table->foreignId('category_id')
->nullable()
->constrained()
->nullOnDelete();
membuat kolom:
category_id
yang terhubung ke:
categories.id
Karena menggunakan:
nullable()
produk masih dapat disimpan tanpa kategori.
Sedangkan:
nullOnDelete()
berarti jika kategori dihapus, produk tidak ikut dihapus.
Nilai category_id pada produk tersebut akan menjadi:
NULL
Pilihan ini lebih aman daripada menghapus seluruh produk hanya karena kategori dihapus.
Penjelasan unit_id
Konsep unit_id sama seperti kategori.
Jika satuan dihapus, produk tetap tersimpan tetapi:
unit_id = NULL
Penjelasan sku
Kolom:
$table->string('sku', 50)->unique();
digunakan sebagai kode unik produk.
Contoh:
BRG-0001
BRG-0002
MNM-001
SKU tidak boleh sama antara satu produk dan produk lainnya.
Harga beli dan harga jual
Kolom:
$table->decimal('purchase_price', 15, 2)->default(0);
$table->decimal('selling_price', 15, 2)->default(0);
menggunakan tipe decimal.
Untuk data uang, tipe decimal lebih tepat daripada float karena nilai disimpan secara presisi sesuai jumlah digit yang ditentukan.
Format:
decimal(15, 2)
berarti tersedia total 15 digit dengan 2 digit di belakang desimal.
Kolom stock
Kolom:
$table->integer('stock')->default(0);
menyimpan saldo stok produk saat ini.
Nilai stok akan berubah ketika terjadi:
- pembelian;
- penjualan;
- penyesuaian stok;
- pembatalan transaksi yang memengaruhi stok.
Pada tutorial ini perubahan stok tidak dilakukan sembarangan melalui form edit produk.
Riwayat perubahannya juga akan dicatat pada tabel:
stock_movements
minimum_stock
Kolom:
$table->integer('minimum_stock')->default(0);
digunakan sebagai batas stok minimum.
Contoh:
stock = 3
minimum_stock = 5
berarti produk dapat ditampilkan sebagai produk dengan stok menipis pada dashboard.
is_active
Kolom:
$table->boolean('is_active')->default(true);
digunakan untuk menonaktifkan produk tanpa harus menghapusnya.
Produk yang tidak aktif nantinya dapat dikecualikan dari halaman kasir.
9. Migration Customers
Cari migration:
xxxx_xx_xx_xxxxxx_create_customers_table.php
Isi dengan:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('phone', 30)->nullable();
$table->text('address')->nullable();
$table->timestamps();
$table->index('name');
$table->index('phone');
});
}
public function down(): void
{
Schema::dropIfExists('customers');
}
};
Pelanggan bersifat opsional pada transaksi penjualan.
Artinya sistem tetap dapat melakukan transaksi untuk pembeli umum tanpa harus memilih pelanggan.
10. Migration Suppliers
Cari migration:
xxxx_xx_xx_xxxxxx_create_suppliers_table.php
Isi dengan:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('suppliers', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('phone', 30)->nullable();
$table->text('address')->nullable();
$table->timestamps();
$table->index('name');
$table->index('phone');
});
}
public function down(): void
{
Schema::dropIfExists('suppliers');
}
};
Supplier digunakan pada transaksi pembelian barang.
Contoh data supplier:
PT Sumber Makmur
CV Bali Sejahtera
UD Maju Bersama
11. Migration Purchases
Cari migration:
xxxx_xx_xx_xxxxxx_create_purchases_table.php
Isi dengan:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('purchases', function (Blueprint $table) {
$table->id();
$table->string('invoice_number')->unique();
$table->foreignId('supplier_id')
->nullable()
->constrained()
->nullOnDelete();
$table->foreignId('user_id')
->constrained()
->restrictOnDelete();
$table->timestamp('purchase_date');
$table->decimal('grand_total', 15, 2)->default(0);
$table->string('status', 20)->default('received');
$table->timestamps();
$table->index('purchase_date');
$table->index('status');
});
}
public function down(): void
{
Schema::dropIfExists('purchases');
}
};
invoice_number
Kolom:
$table->string('invoice_number')->unique();
menyimpan nomor transaksi pembelian.
Contoh:
PUR-20260916-0001
Nomor invoice harus unik.
supplier_id
Supplier dibuat nullable:
nullable()
agar transaksi lama tidak ikut hilang jika supplier suatu saat dihapus.
Dengan:
nullOnDelete()
jika supplier dihapus maka transaksi pembelian tetap ada dan supplier_id menjadi NULL.
user_id
Kode:
$table->foreignId('user_id')
->constrained()
->restrictOnDelete();
menghubungkan transaksi dengan pengguna yang membuat transaksi.
Berbeda dari supplier, pengguna yang sudah memiliki transaksi tidak boleh dihapus secara sembarangan.
Karena itu digunakan:
restrictOnDelete()
Dengan pendekatan ini histori transaksi tetap memiliki jejak pengguna yang membuatnya.
Jika akun tidak digunakan lagi, pada aplikasi kita cukup mengubah:
is_active = false
daripada menghapus pengguna.
purchase_date
Kolom:
$table->timestamp('purchase_date');
menyimpan tanggal dan waktu transaksi pembelian.
status pembelian
Status awal:
received
menandakan barang sudah diterima dan stok sudah ditambahkan.
Nantinya status dapat digunakan untuk membedakan transaksi aktif dan transaksi yang dibatalkan.
12. Migration Purchase Details
Cari migration:
xxxx_xx_xx_xxxxxx_create_purchase_details_table.php
Isi dengan:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('purchase_details', function (Blueprint $table) {
$table->id();
$table->foreignId('purchase_id')
->constrained()
->cascadeOnDelete();
$table->foreignId('product_id')
->constrained()
->restrictOnDelete();
$table->integer('quantity');
$table->decimal('price', 15, 2);
$table->decimal('subtotal', 15, 2);
$table->timestamps();
$table->index(['purchase_id', 'product_id']);
});
}
public function down(): void
{
Schema::dropIfExists('purchase_details');
}
};
purchase_id
Detail transaksi harus selalu mempunyai header pembelian.
Karena itu:
$table->foreignId('purchase_id')
->constrained()
->cascadeOnDelete();
Jika sebuah header pembelian benar-benar dihapus, detailnya ikut dihapus.
Namun pada implementasi sistem kasir nanti, transaksi yang sudah memengaruhi stok sebaiknya tidak dihapus langsung.
Pembatalan transaksi akan ditangani melalui status dan pembalikan stok agar histori tetap dapat ditelusuri.
product_id
Untuk produk digunakan:
restrictOnDelete()
Artinya produk yang sudah pernah digunakan dalam transaksi tidak dapat dihapus begitu saja.
Cara yang lebih aman adalah menonaktifkan produk menggunakan:
is_active = false
quantity
Kolom:
$table->integer('quantity');
menyimpan jumlah barang yang dibeli.
Contoh:
10
25
100
Validasi agar jumlah selalu lebih dari nol akan dilakukan pada controller saat membuat transaksi.
price
Kolom:
$table->decimal('price', 15, 2);
menyimpan harga beli produk pada saat transaksi terjadi.
Harga disimpan pada detail transaksi karena harga produk dapat berubah di masa depan.
Dengan demikian transaksi lama tetap memiliki harga historisnya.
subtotal
Subtotal menyimpan:
quantity × price
Nilai ini akan dihitung ulang di sisi server ketika transaksi dibuat.
13. Migration Sales
Cari migration:
xxxx_xx_xx_xxxxxx_create_sales_table.php
Isi dengan:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('sales', function (Blueprint $table) {
$table->id();
$table->string('invoice_number')->unique();
$table->foreignId('user_id')
->constrained()
->restrictOnDelete();
$table->foreignId('customer_id')
->nullable()
->constrained()
->nullOnDelete();
$table->timestamp('sale_date');
$table->decimal('subtotal', 15, 2)->default(0);
$table->decimal('discount', 15, 2)->default(0);
$table->decimal('grand_total', 15, 2)->default(0);
$table->decimal('paid_amount', 15, 2)->default(0);
$table->decimal('change_amount', 15, 2)->default(0);
$table->string('status', 20)->default('paid');
$table->text('notes')->nullable();
$table->timestamps();
$table->index('sale_date');
$table->index('status');
$table->index(['sale_date', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('sales');
}
};
user_id
user_id menunjukkan pengguna yang melakukan transaksi.
Biasanya pengguna tersebut adalah kasir atau administrator.
customer_id
Pelanggan dibuat opsional.
Artinya transaksi dapat dilakukan tanpa memilih pelanggan.
Jika pelanggan dihapus, histori penjualan tetap dipertahankan dan:
customer_id = NULL
subtotal
subtotal menyimpan total item sebelum diskon transaksi.
Contoh:
Produk A = Rp20.000
Produk B = Rp30.000
subtotal = Rp50.000
discount
discount menyimpan potongan transaksi secara keseluruhan.
Pada implementasi controller, nilai diskon akan divalidasi agar tidak menghasilkan total negatif.
grand_total
Secara sederhana:
grand_total = subtotal - discount
Perhitungan akhir dilakukan di server.
Jangan mempercayai total yang dikirim dari browser.
paid_amount
paid_amount adalah uang yang diberikan pelanggan.
Contoh:
grand_total = 45000
paid_amount = 50000
change_amount
Kembalian dihitung:
change_amount = paid_amount - grand_total
Pada contoh sebelumnya:
50000 - 45000 = 5000
status
Status awal adalah:
paid
Status nantinya dapat digunakan antara lain untuk:
paid
cancelled
Transaksi yang dibatalkan tidak boleh dihitung sebagai pendapatan pada laporan.
14. Migration Sale Details
Cari migration:
xxxx_xx_xx_xxxxxx_create_sale_details_table.php
Isi dengan:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('sale_details', function (Blueprint $table) {
$table->id();
$table->foreignId('sale_id')
->constrained()
->cascadeOnDelete();
$table->foreignId('product_id')
->constrained()
->restrictOnDelete();
$table->integer('quantity');
$table->decimal('price', 15, 2);
$table->decimal('discount', 15, 2)->default(0);
$table->decimal('subtotal', 15, 2);
$table->timestamps();
$table->index(['sale_id', 'product_id']);
});
}
public function down(): void
{
Schema::dropIfExists('sale_details');
}
};
Tabel ini menyimpan setiap produk yang terdapat pada satu transaksi penjualan.
Contoh:
sales
id = 10
invoice_number = INV-001
grand_total = 50000
mempunyai detail:
sale_details
sale_id = 10
product_id = 1
quantity = 2
price = 10000
subtotal = 20000
sale_id = 10
product_id = 2
quantity = 1
price = 30000
subtotal = 30000
Total kedua detail tersebut:
20000 + 30000 = 50000
Mengapa harga disimpan lagi pada sale_details?
Walaupun tabel products sudah mempunyai:
selling_price
harga tetap harus disimpan pada sale_details.
Misalnya hari ini produk dijual:
Rp10.000
Bulan depan harga berubah menjadi:
Rp12.000
Transaksi lama harus tetap menunjukkan harga:
Rp10.000
Karena itu detail transaksi menyimpan snapshot harga saat transaksi terjadi.
15. Migration Stock Movements
Cari migration:
xxxx_xx_xx_xxxxxx_create_stock_movements_table.php
Isi dengan:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('stock_movements', function (Blueprint $table) {
$table->id();
$table->foreignId('product_id')
->constrained()
->restrictOnDelete();
$table->string('type', 30);
$table->integer('quantity_in')->default(0);
$table->integer('quantity_out')->default(0);
$table->string('reference_type', 50)->nullable();
$table->unsignedBigInteger('reference_id')->nullable();
$table->text('notes')->nullable();
$table->timestamps();
$table->index('type');
$table->index(['reference_type', 'reference_id']);
$table->index(['product_id', 'created_at']);
});
}
public function down(): void
{
Schema::dropIfExists('stock_movements');
}
};
Tabel ini sangat penting karena digunakan sebagai histori perubahan stok.
Saldo stok terbaru tetap tersimpan pada:
products.stock
Sedangkan stock_movements digunakan untuk mengetahui mengapa stok berubah.
Contoh stok masuk
Pembelian 10 unit:
type = purchase
quantity_in = 10
quantity_out = 0
Contoh stok keluar
Penjualan 2 unit:
type = sale
quantity_in = 0
quantity_out = 2
Contoh penyesuaian stok
Jika stok fisik lebih banyak 3 unit daripada stok sistem:
type = adjustment
quantity_in = 3
quantity_out = 0
Jika stok fisik berkurang 2 unit:
type = adjustment
quantity_in = 0
quantity_out = 2
reference_type dan reference_id
Kolom:
reference_type
reference_id
digunakan untuk mengetahui transaksi sumber pergerakan stok.
Contoh transaksi penjualan:
reference_type = sale
reference_id = 15
Contoh pembelian:
reference_type = purchase
reference_id = 8
Kita sengaja tidak membuat foreign key langsung untuk reference_id karena satu kolom tersebut dapat merujuk ke beberapa jenis transaksi.
Pada tahap berikutnya nilai tersebut akan diisi melalui logic transaksi.
16. Migration Settings
Cari migration:
xxxx_xx_xx_xxxxxx_create_settings_table.php
Isi dengan:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('settings', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->text('value')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('settings');
}
};
Tabel ini menggunakan format sederhana:
key
value
Contoh:
| key | value |
|---|---|
| store_name | Toko RumahSourceCode |
| store_address | Denpasar |
| store_phone | 081234567890 |
| receipt_footer | Terima kasih telah berbelanja |
Pendekatan key-value membuat pengaturan sederhana dapat ditambah tanpa harus menambah kolom baru setiap kali ada konfigurasi tambahan.
17. Memeriksa Urutan File Migration
Buka folder:
database/migrations
Pastikan migration induk berada sebelum migration yang menggunakannya.
Contoh urutan yang aman:
xxxx_xx_xx_xxxxxx_create_users_table.php
xxxx_xx_xx_xxxxxx_add_role_and_is_active_to_users_table.php
xxxx_xx_xx_xxxxxx_create_categories_table.php
xxxx_xx_xx_xxxxxx_create_units_table.php
xxxx_xx_xx_xxxxxx_create_products_table.php
xxxx_xx_xx_xxxxxx_create_customers_table.php
xxxx_xx_xx_xxxxxx_create_suppliers_table.php
xxxx_xx_xx_xxxxxx_create_purchases_table.php
xxxx_xx_xx_xxxxxx_create_purchase_details_table.php
xxxx_xx_xx_xxxxxx_create_sales_table.php
xxxx_xx_xx_xxxxxx_create_sale_details_table.php
xxxx_xx_xx_xxxxxx_create_stock_movements_table.php
xxxx_xx_xx_xxxxxx_create_settings_table.php
Yang paling penting adalah:
categories sebelum products
units sebelum products
products sebelum purchase_details
products sebelum sale_details
products sebelum stock_movements
suppliers sebelum purchases
customers sebelum sales
purchases sebelum purchase_details
sales sebelum sale_details
users sebelum purchases
users sebelum sales
18. Menjalankan Migration
Setelah semua migration selesai, jalankan:
php artisan migrate
Jika berhasil, terminal akan menampilkan daftar migration dengan status:
DONE
Contoh:
create_categories_table ................. DONE
create_units_table ...................... DONE
create_products_table ................... DONE
create_customers_table .................. DONE
create_suppliers_table .................. DONE
create_purchases_table .................. DONE
create_purchase_details_table ........... DONE
create_sales_table ...................... DONE
create_sale_details_table ............... DONE
create_stock_movements_table ............ DONE
create_settings_table ................... DONE
19. Memeriksa Status Migration
Jalankan:
php artisan migrate:status
Pastikan migration sistem kasir memiliki status:
Ran
Jika ada migration berstatus:
Pending
jalankan kembali:
php artisan migrate
20. Melihat SQL Sebelum Migration
Saat ingin memeriksa SQL yang akan dijalankan tanpa langsung mengubah database, Laravel menyediakan opsi:
php artisan migrate --pretend
Perintah ini berguna ketika ingin melihat query yang akan dieksekusi oleh migration.
Untuk pemula bagian ini tidak wajib, tetapi berguna saat melakukan debugging.
21. Memeriksa Tabel Melalui phpMyAdmin
Jika menggunakan phpMyAdmin, buka database:
kasir_laravel
Pastikan tabel berikut tersedia:
categories
customers
migrations
password_reset_tokens
products
purchase_details
purchases
sale_details
sales
sessions
settings
stock_movements
suppliers
units
users
Beberapa tabel bawaan Laravel lain dapat berbeda tergantung instalasi project.
Yang penting seluruh tabel bisnis yang dibuat pada tahap ini sudah tersedia.
22. Memeriksa Struktur Products
Buka tabel:
products
Pastikan minimal terdapat kolom:
id
category_id
unit_id
sku
name
purchase_price
selling_price
stock
minimum_stock
is_active
created_at
updated_at
Periksa juga foreign key:
category_id -> categories.id
unit_id -> units.id
23. Memeriksa Struktur Sales
Buka tabel:
sales
Pastikan terdapat:
id
invoice_number
user_id
customer_id
sale_date
subtotal
discount
grand_total
paid_amount
change_amount
status
notes
created_at
updated_at
Foreign key utamanya:
user_id -> users.id
customer_id -> customers.id
24. Memeriksa Struktur Sale Details
Buka:
sale_details
Pastikan foreign key:
sale_id -> sales.id
product_id -> products.id
Tabel detail inilah yang memungkinkan satu transaksi mempunyai banyak produk.
25. Memeriksa Struktur Purchases
Buka:
purchases
Pastikan foreign key:
supplier_id -> suppliers.id
user_id -> users.id
26. Memeriksa Struktur Purchase Details
Buka:
purchase_details
Pastikan:
purchase_id -> purchases.id
product_id -> products.id
27. Memahami Aturan Penghapusan Foreign Key
Pada struktur ini kita menggunakan tiga perilaku utama:
nullOnDelete()
restrictOnDelete()
cascadeOnDelete()
nullOnDelete
Digunakan pada relasi yang boleh kehilangan data induk tanpa menghapus transaksi utama.
Contoh:
customer_id
supplier_id
category_id
unit_id
Jika data induk dihapus, foreign key menjadi:
NULL
restrictOnDelete
Digunakan jika data induk tidak boleh dihapus ketika sudah digunakan.
Contoh:
sales.user_id
purchases.user_id
sale_details.product_id
purchase_details.product_id
stock_movements.product_id
Pendekatan ini membantu menjaga histori transaksi.
cascadeOnDelete
Digunakan pada detail yang bergantung penuh pada header.
Contoh:
sale_details.sale_id
purchase_details.purchase_id
Jika header dihapus secara fisik, seluruh detailnya ikut terhapus.
Walaupun demikian, pada aplikasi kasir kita tidak akan menjadikan penghapusan fisik transaksi sebagai cara utama membatalkan transaksi.
Transaksi yang sudah mengubah stok sebaiknya dibatalkan melalui proses yang dapat mengembalikan stok dan mencatat histori.
28. Mengapa Produk Tidak Sebaiknya Dihapus Setelah Dipakai Transaksi?
Misalnya produk:
Kopi Bali
sudah digunakan pada 1.000 transaksi.
Jika produk tersebut dihapus paksa, histori transaksi berpotensi kehilangan referensi produk.
Karena itu sistem menyediakan:
is_active
Produk yang tidak dijual lagi cukup dinonaktifkan:
is_active = false
Data transaksi lama tetap utuh.
Pendekatan yang sama berlaku untuk akun pengguna.
Akun kasir lama sebaiknya dinonaktifkan daripada dihapus jika sudah mempunyai histori transaksi.
29. Mengapa Products Memiliki Kolom Stock dan Stock Movements?
Pada sistem ini terdapat dua konsep:
products.stock
dan:
stock_movements
products.stock digunakan untuk mendapatkan saldo stok terbaru dengan cepat.
Contoh:
Produk A
stock = 25
Sedangkan stock_movements digunakan sebagai histori.
Contoh:
+50 pembelian
-10 penjualan
-10 penjualan
-5 penyesuaian
Hasil akhirnya:
25
Dengan cara ini aplikasi tidak perlu menjumlahkan seluruh histori stok setiap kali halaman kasir dibuka.
Namun setiap perubahan stok penting harus dicatat agar saldo dapat ditelusuri.
30. Mengapa Tidak Mengubah Stock Langsung dari CRUD Produk?
Pada awal pembuatan produk kita akan menggunakan stok awal:
0
Penambahan stok selanjutnya dilakukan melalui:
- pembelian;
- penyesuaian stok.
Tujuannya agar perubahan stok memiliki histori.
Jika administrator dapat mengubah:
stock = 100
langsung dari form edit produk tanpa catatan, kita tidak mengetahui asal perubahan stok tersebut.
Karena itu stok akan dikelola melalui proses khusus pada tahap pembelian dan manajemen stok.
31. Memahami Header dan Detail Transaksi
Salah satu konsep penting database kasir adalah:
header-detail
Misalnya transaksi:
INV-001
Kasir: Admin
Tanggal: 16 September 2026
Pelanggan: Umum
informasi tersebut disimpan pada:
sales
Sedangkan produk:
2 x Kopi
1 x Susu
3 x Air Mineral
disimpan pada:
sale_details
Dengan demikian satu record sales dapat mempunyai banyak record sale_details.
Konsep yang sama digunakan pada pembelian:
purchases
purchase_details
32. Contoh Data Penjualan Secara Konseptual
Contoh header:
sales
id = 1
invoice_number = INV-0001
user_id = 1
customer_id = NULL
subtotal = 50000
discount = 5000
grand_total = 45000
paid_amount = 50000
change_amount = 5000
status = paid
Detail:
sale_details
sale_id = 1
product_id = 1
quantity = 2
price = 10000
discount = 0
subtotal = 20000
dan:
sale_id = 1
product_id = 2
quantity = 1
price = 30000
discount = 0
subtotal = 30000
Total detail:
20000 + 30000 = 50000
Kemudian diskon header:
5000
menghasilkan:
grand_total = 45000
33. Contoh Data Pembelian Secara Konseptual
Header:
purchases
id = 1
invoice_number = PUR-0001
supplier_id = 1
user_id = 1
grand_total = 250000
status = received
Detail:
purchase_details
purchase_id = 1
product_id = 1
quantity = 10
price = 10000
subtotal = 100000
dan:
purchase_id = 1
product_id = 2
quantity = 15
price = 10000
subtotal = 150000
Total:
100000 + 150000 = 250000
Ketika pembelian disimpan, stok produk akan ditambah pada tahap transaksi pembelian nanti.
34. Mengecek Database Menggunakan Artisan Tinker
Kita juga dapat memastikan tabel tersedia menggunakan Tinker.
Jalankan:
php artisan tinker
Kemudian:
Schema::hasTable('products');
Jika tabel tersedia, hasilnya:
true
Cek tabel lain:
Schema::hasTable('sales');
Schema::hasTable('sale_details');
Schema::hasTable('purchases');
Schema::hasTable('purchase_details');
Schema::hasTable('stock_movements');
Keluar dari Tinker:
exit
35. Mengecek Kolom dengan Tinker
Masuk kembali jika diperlukan:
php artisan tinker
Cek kolom:
Schema::hasColumn('products', 'sku');
Hasil yang diharapkan:
true
Cek:
Schema::hasColumn('products', 'stock');
Schema::hasColumn('sales', 'invoice_number');
Schema::hasColumn('purchases', 'supplier_id');
Keluar:
exit
36. Jika Migration Belum Pernah Dipakai pada Data Penting
Saat masih tahap awal pengembangan dan database belum berisi data penting, kita dapat mengulang seluruh migration menggunakan:
php artisan migrate:fresh
Perintah ini akan menghapus seluruh tabel dan menjalankan kembali semua migration.
Peringatan: seluruh data pada database tersebut akan terhapus.
Jangan menjalankan:
php artisan migrate:fresh
pada database produksi yang sudah memiliki data transaksi.
Untuk project tutorial yang masih kosong, perintah ini berguna ketika struktur migration masih sering diperbaiki.
37. Jika Hanya Ingin Membatalkan Migration Terakhir
Gunakan:
php artisan migrate:rollback
Perintah tersebut menjalankan method:
down()
dari batch migration terakhir.
Karena itulah setiap migration pada tutorial ini mempunyai method:
public function down(): void
yang berisi:
Schema::dropIfExists(...);
38. Error Foreign Key Constraint Saat Migration
Salah satu error yang sering terjadi adalah foreign key mengarah ke tabel yang belum dibuat.
Contoh penyebab:
sale_details dibuat sebelum sales
atau:
products dibuat sebelum categories
Solusi saat database masih tahap pengembangan:
- periksa timestamp nama migration;
- pastikan tabel induk dibuat terlebih dahulu;
- perbaiki urutan migration;
- jalankan:
php artisan migrate:fresh
Ingat, migrate:fresh menghapus seluruh tabel.
Gunakan hanya jika data masih aman untuk dihapus.
39. Error Table Already Exists
Jika muncul pesan seperti:
Table 'products' already exists
biasanya tabel sudah pernah dibuat tetapi status migration tidak sesuai.
Pada project latihan yang belum mempunyai data penting, solusi termudah:
php artisan migrate:fresh
Jika database sudah berisi data penting, jangan langsung menjalankan perintah tersebut.
Periksa migration dan kondisi database terlebih dahulu.
40. Error Cannot Add Foreign Key Constraint
Jika MySQL menampilkan error foreign key, periksa beberapa hal:
- tabel induk sudah tersedia;
- nama tabel benar;
- nama kolom foreign key benar;
- tipe kolom induk dan foreign key sesuai;
- migration berjalan dalam urutan yang benar.
Contoh:
$table->foreignId('product_id')->constrained();
secara default mengarah ke:
products.id
Jika nama tabel tidak mengikuti konvensi Laravel, target tabel harus ditentukan secara eksplisit.
Pada tutorial ini seluruh nama tabel menggunakan konvensi Laravel sehingga constrained() dapat digunakan langsung.
41. Error Unknown Database
Jika muncul error:
Unknown database 'kasir_laravel'
berarti database belum dibuat atau nama database pada .env salah.
Pastikan database:
kasir_laravel
sudah tersedia.
Kemudian cek:
DB_DATABASE=kasir_laravel
Bersihkan cache:
php artisan config:clear
Lalu:
php artisan migrate
42. Error Access Denied for MySQL
Jika muncul:
Access denied for user
periksa:
DB_USERNAME=
DB_PASSWORD=
Pastikan sesuai dengan akun MySQL yang digunakan.
Setelah mengubah .env:
php artisan config:clear
43. Jangan Mengubah Migration Lama Setelah Aplikasi Produksi
Selama tutorial masih dalam tahap pengembangan lokal, migration dapat diperbaiki lalu dijalankan ulang menggunakan:
php artisan migrate:fresh
Namun setelah aplikasi sudah dipakai dan migration tersebut sudah berjalan di server produksi, jangan mengubah struktur lama lalu berharap Laravel menjalankannya ulang.
Untuk perubahan berikutnya, buat migration baru.
Contoh jika nanti ingin menambah kolom barcode:
php artisan make:migration add_barcode_to_products_table --table=products
Kemudian perubahan dilakukan pada migration baru tersebut.
Pendekatan ini menjaga histori perubahan database.
44. Struktur Database Akhir
Setelah tahap ini selesai, struktur utama adalah:
users
├── id
├── name
├── email
├── password
├── role
└── is_active
categories
├── id
└── name
units
├── id
├── name
└── symbol
products
├── id
├── category_id
├── unit_id
├── sku
├── name
├── purchase_price
├── selling_price
├── stock
├── minimum_stock
└── is_active
customers
├── id
├── name
├── phone
└── address
suppliers
├── id
├── name
├── phone
└── address
sales
├── id
├── invoice_number
├── user_id
├── customer_id
├── sale_date
├── subtotal
├── discount
├── grand_total
├── paid_amount
├── change_amount
├── status
└── notes
sale_details
├── id
├── sale_id
├── product_id
├── quantity
├── price
├── discount
└── subtotal
purchases
├── id
├── invoice_number
├── supplier_id
├── user_id
├── purchase_date
├── grand_total
└── status
purchase_details
├── id
├── purchase_id
├── product_id
├── quantity
├── price
└── subtotal
stock_movements
├── id
├── product_id
├── type
├── quantity_in
├── quantity_out
├── reference_type
├── reference_id
└── notes
settings
├── id
├── key
└── value
Setiap tabel juga mempunyai kolom timestamp Laravel:
created_at
updated_at
kecuali jika struktur bawaan tertentu berbeda.
45. Checklist Pengujian Tahap 02
Sebelum melanjutkan, pastikan semua poin berikut berhasil.
Pengujian 1 — Migration berjalan
Jalankan:
php artisan migrate
Tidak boleh ada error.
Pengujian 2 — Status migration
Jalankan:
php artisan migrate:status
Seluruh migration bisnis harus:
Ran
Pengujian 3 — Tabel produk tersedia
Jalankan:
php artisan tinker
Kemudian:
Schema::hasTable('products');
Hasil:
true
Pengujian 4 — Tabel transaksi tersedia
Cek:
Schema::hasTable('sales');
Schema::hasTable('sale_details');
Schema::hasTable('purchases');
Schema::hasTable('purchase_details');
Semua harus:
true
Pengujian 5 — Tabel stok tersedia
Cek:
Schema::hasTable('stock_movements');
Hasil:
true
Pengujian 6 — Kolom penting tersedia
Cek:
Schema::hasColumn('products', 'sku');
Schema::hasColumn('products', 'stock');
Schema::hasColumn('sales', 'grand_total');
Schema::hasColumn('purchases', 'grand_total');
Semua harus:
true
Keluar dari Tinker:
exit
46. Checkpoint Hasil
Jika tahap ini berhasil, kondisi aplikasi sekarang adalah:
Login : selesai
Role admin/kasir : selesai
Middleware role : selesai
Database MySQL : terhubung
Tabel master : selesai
Tabel penjualan : selesai
Tabel pembelian : selesai
Tabel stok : selesai
Tabel settings : selesai
Model Eloquent : belum dikonfigurasi
Relasi Eloquent : belum dikonfigurasi
Seeder : belum dibuat
CRUD : belum dibuat
Transaksi : belum dibuat
Ini merupakan kondisi yang benar.
Pada tahap berikutnya kita akan mengisi model dan membuat relasi Eloquent antar tabel.
Kesalahan Umum
Beberapa kesalahan yang sering terjadi pada tahap pembuatan database:
1. Migration detail dibuat lebih dahulu
Contoh salah:
sale_details
sales
Yang benar:
sales
sale_details
2. Products dibuat sebelum categories dan units
Karena products menggunakan foreign key kategori dan satuan, tabel induk harus tersedia lebih dahulu.
3. Menghapus produk yang sudah mempunyai transaksi
Produk yang sudah pernah dipakai transaksi sebaiknya dinonaktifkan, bukan dihapus.
4. Mengubah stok langsung tanpa histori
Perubahan stok selanjutnya akan dilakukan melalui pembelian atau penyesuaian stok agar mempunyai catatan pada stock_movements.
5. Menjalankan migrate:fresh pada database produksi
Perintah:
php artisan migrate:fresh
menghapus seluruh tabel.
Gunakan hanya jika memang seluruh data boleh dihapus.
6. Menggunakan float untuk nilai uang
Pada tutorial ini nilai uang menggunakan:
decimal(15, 2)
agar penyimpanan nilai lebih terkontrol untuk kebutuhan transaksi.
7. Menghapus user yang sudah mempunyai transaksi
Akun lama lebih aman dinonaktifkan dengan:
is_active = false
agar histori pengguna pada transaksi tetap tersedia.
Ringkasan
Pada tahap kedua kita telah membuat seluruh struktur database utama Sistem Kasir Laravel.
Tabel master yang dibuat:
categories
units
products
customers
suppliers
Tabel transaksi:
sales
sale_details
purchases
purchase_details
Tabel pendukung:
stock_movements
settings
Kita juga sudah memahami penggunaan:
foreign key
nullOnDelete
restrictOnDelete
cascadeOnDelete
serta konsep header-detail pada transaksi penjualan dan pembelian.
Database sekarang sudah siap digunakan oleh Eloquent.
Pada tahap berikutnya kita akan mengatur $fillable, casting, relasi antar model, serta membuat seeder data awal agar aplikasi mempunyai kategori, satuan, produk contoh, pengaturan toko, dan akun yang dapat digunakan untuk pengujian.





Komentar
Tulis Komentar