Bagian 2 dari seri

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:

  • users sebagai pengguna sistem yang sudah tersedia dari tahap sebelumnya;
  • categories untuk kategori produk;
  • units untuk satuan produk;
  • products untuk data barang;
  • customers untuk data pelanggan;
  • suppliers untuk data pemasok;
  • sales untuk header transaksi penjualan;
  • sale_details untuk item penjualan;
  • purchases untuk header transaksi pembelian;
  • purchase_details untuk item pembelian;
  • stock_movements untuk riwayat stok masuk dan keluar;
  • settings untuk 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:

  1. periksa timestamp nama migration;
  2. pastikan tabel induk dibuat terlebih dahulu;
  3. perbaiki urutan migration;
  4. 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.

Lanjut: Model, Relasi, dan Data Awal

Komentar

Belum ada komentar.

Tulis Komentar

Komentar pengunjung diperiksa sebelum ditampilkan.