Penjualan adalah momen paling rawan kesalahan data di seluruh toko: satu pesanan menyentuh tiga tabel sekaligus — kepala pesanan, baris-baris pesanan_item, dan stok di produk. Bila aplikasi berhenti di tengah jalan, database tidak boleh menyimpan pesanan tanpa item, item tanpa kepala, atau stok yang sudah berkurang padahal pesanannya gagal. Pada bagian ini kita membangun alur penjualan lengkap: pembuatan pesanan multi-item dalam satu transaksi atomik dengan snapshot harga, penolakan pesanan yang menuntut melebihi stok, bukti bahwa transaksi yang gagal tidak meninggalkan jejak, dan penutupan pesanan lewat alur status yang terjaga.
Papan Stok Awal #
Berkas pembuka membangun ulang skema dan data master dari Bagian 1, kali ini tanpa pesanan historis — kita berdiri pada kondisi toko yang siap berjualan. Sepuluh baris terakhir adalah papan stok yang akan diuji oleh skenario bagian ini; simpan angka-angkanya, karena setiap transaksi berikutnya harus mengubahnya dengan tepat:
-- siapkan.sql — fondasi Bagian 3: skema dari Bagian 1 + data master
-- dengan stok awal toko. Bagian ini dimaksudkan dapat dijalankan
-- sendiri dari database kosong.
CREATE TABLE kategori (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL UNIQUE
);
CREATE TABLE produk (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kode text NOT NULL UNIQUE,
nama text NOT NULL,
kategori_id integer NOT NULL REFERENCES kategori (id),
harga_beli numeric(12, 2) NOT NULL CHECK (harga_beli > 0),
harga_jual numeric(12, 2) NOT NULL CHECK (harga_jual > 0),
stok integer NOT NULL CHECK (stok >= 0)
);
CREATE TABLE pelanggan (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL,
email text NOT NULL UNIQUE,
kota text NOT NULL
);
CREATE TABLE pesanan (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pelanggan_id integer NOT NULL REFERENCES pelanggan (id),
tanggal date NOT NULL,
status text NOT NULL DEFAULT 'baru'
CHECK (status IN ('baru', 'diproses', 'selesai', 'batal'))
);
CREATE TABLE pesanan_item (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pesanan_id integer NOT NULL REFERENCES pesanan (id) ON DELETE CASCADE,
produk_id integer NOT NULL REFERENCES produk (id),
qty integer NOT NULL CHECK (qty > 0),
harga_saat_transaksi numeric(12, 2) NOT NULL,
subtotal numeric(12, 2) GENERATED ALWAYS AS (qty * harga_saat_transaksi) STORED
);
CREATE INDEX idx_pesanan_tanggal ON pesanan (tanggal);
CREATE INDEX idx_pesanan_pelanggan ON pesanan (pelanggan_id);
CREATE INDEX idx_pesanan_item_pesanan ON pesanan_item (pesanan_id);
CREATE INDEX idx_pesanan_item_produk ON pesanan_item (produk_id);
INSERT INTO kategori (nama) VALUES
('Sembako'),
('Minuman'),
('Makanan'),
('Kebersihan');
INSERT INTO produk (kode, nama, kategori_id, harga_beli, harga_jual, stok) VALUES
('PRD-001', 'Beras Premium 5kg', 1, 61000.00, 68000.00, 40),
('PRD-002', 'Gula Pasir 1kg', 1, 15000.00, 17000.00, 60),
('PRD-003', 'Minyak Goreng 2L', 1, 32000.00, 36500.00, 35),
('PRD-004', 'Telur Ayam 1kg', 1, 27000.00, 31000.00, 25),
('PRD-005', 'Kopi Sachet Instan', 2, 9000.00, 12000.00, 80),
('PRD-006', 'Teh Celup 50 kantong', 2, 11000.00, 13500.00, 45),
('PRD-007', 'Air Mineral 600ml', 2, 2500.00, 4000.00, 150),
('PRD-008', 'Mie Instan Goreng', 3, 2800.00, 3500.00, 100),
('PRD-009', 'Biskuit Cokelat', 3, 7500.00, 9500.00, 30),
('PRD-010', 'Sabun Cuci Piring 780ml', 4, 16000.00, 19500.00, 22);
INSERT INTO pelanggan (nama, email, kota) VALUES
('Budi Santoso', '[email protected]', 'Bandung'),
('Siti Rahayu', '[email protected]', 'Bandung'),
('Andi Wijaya', '[email protected]', 'Jakarta'),
('Dewi Lestari', '[email protected]', 'Surabaya'),
('Rudi Hartono', '[email protected]', 'Bandung'),
('Maya Putri', '[email protected]', 'Jakarta');
-- papan stok awal: angka-angka ini yang diuji oleh skenario penjualan
SELECT kode, nama, stok
FROM produk
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE INDEX
CREATE INDEX
CREATE INDEX
CREATE INDEX
INSERT 0 4
INSERT 0 10
INSERT 0 6
kode | nama | stok
---------+-------------------------+------
PRD-001 | Beras Premium 5kg | 40
PRD-002 | Gula Pasir 1kg | 60
PRD-003 | Minyak Goreng 2L | 35
PRD-004 | Telur Ayam 1kg | 25
PRD-005 | Kopi Sachet Instan | 80
PRD-006 | Teh Celup 50 kantong | 45
PRD-007 | Air Mineral 600ml | 150
PRD-008 | Mie Instan Goreng | 100
PRD-009 | Biskuit Cokelat | 30
PRD-010 | Sabun Cuci Piring 780ml | 22
(10 rows)Satu Pesanan, Satu Transaksi #
Siti datang pada 5 April 2026 dan memesan tiga produk sekaligus: sepuluh kopi, lima gula, dan dua puluh mie instan. Seluruh prosesnya — kepala pesanan, tiga baris item dengan snapshot harga, dan pengurangan stok tiga produk — berlangsung dalam satu blok BEGIN ... COMMIT:
-- buat-pesanan.sql — satu pesanan multi-item dalam SATU transaksi:
-- kepala pesanan, item dengan snapshot harga, dan pengurangan stok
-- terjadi bersama-sama atau tidak sama sekali.
BEGIN;
-- 1) kepala pesanan: Siti, 5 April 2026, status awal 'baru'
INSERT INTO pesanan (pelanggan_id, tanggal, status)
VALUES (2, DATE '2026-04-05', 'baru');
-- 2) item pesanan: harga DISALIN dari produk saat transaksi berlangsung.
-- Kolom harga_saat_transaksi adalah snapshot: harga di tabel produk
-- boleh berubah besok tanpa merusak nilai pesanan yang sudah lewat.
INSERT INTO pesanan_item (pesanan_id, produk_id, qty, harga_saat_transaksi)
SELECT p.id, pr.id, v.qty, pr.harga_jual
FROM (VALUES
('PRD-005', 10),
('PRD-002', 5),
('PRD-008', 20)
) AS v(kode_produk, qty)
JOIN pesanan p ON p.pelanggan_id = 2 AND p.tanggal = DATE '2026-04-05'
JOIN produk pr ON pr.kode = v.kode_produk;
-- 3) turunkan stok semua produk yang terjual
UPDATE produk pr
SET stok = pr.stok - i.qty
FROM pesanan_item i
WHERE i.pesanan_id = (SELECT id FROM pesanan
WHERE pelanggan_id = 2 AND tanggal = DATE '2026-04-05')
AND i.produk_id = pr.id;
COMMIT;
-- 4) verifikasi isi pesanan; subtotal dihitung kolom generated
SELECT p.id AS pesanan, pr.kode, pr.nama, i.qty,
i.harga_saat_transaksi, i.subtotal
FROM pesanan_item i
JOIN pesanan p ON p.id = i.pesanan_id
JOIN produk pr ON pr.id = i.produk_id
WHERE p.tanggal = DATE '2026-04-05'
ORDER BY pr.kode;
-- 5) stok ketiga produk setelah penjualan
SELECT pr.kode, pr.nama, pr.stok
FROM produk pr
WHERE pr.kode IN ('PRD-002', 'PRD-005', 'PRD-008')
ORDER BY pr.kode;Output yang Dihasilkan:
BEGIN
INSERT 0 1
INSERT 0 3
UPDATE 3
COMMIT
pesanan | kode | nama | qty | harga_saat_transaksi | subtotal
---------+---------+--------------------+-----+----------------------+-----------
1 | PRD-002 | Gula Pasir 1kg | 5 | 17000.00 | 85000.00
1 | PRD-005 | Kopi Sachet Instan | 10 | 12000.00 | 120000.00
1 | PRD-008 | Mie Instan Goreng | 20 | 3500.00 | 70000.00
(3 rows)
kode | nama | stok
---------+--------------------+------
PRD-002 | Gula Pasir 1kg | 55
PRD-005 | Kopi Sachet Instan | 70
PRD-008 | Mie Instan Goreng | 80
(3 rows)Bedah alurnya langkah demi langkah:
BEGINmembuka transaksi. Sejak titik ini, seluruh perubahan bersifat tentatif: hanya terlihat oleh sesi ini sendiri sampaiCOMMIT.INSERTkepala pesanan menyimpan satu baris informasi transaksi — pelanggan Siti (id 2), tanggal tetap, dan statusbaru— yang menjadi induk bagi seluruh item.INSERT ... SELECTuntuk item memakai tabel turunanVALUESberisi kode produk dan qty, lalu join keproduk— dan inilah letak pola snapshot: kolom yang diisikan bukan sekadarpr.iddanv.qty, tetapi jugapr.harga_jualyang disalin keharga_saat_transaksi. Mulai baris ini tersimpan, harga di katalog boleh berubah bebas; nilai pesanan tidak akan ikut berubah.UPDATE ... FROMmengurangi stok secara set-based: setiap produk yang muncul di item pesanan turun sebanyak qty-nya dalam satu perintah, bukan tiga perintah terpisah. PenjagaCHECK (stok >= 0)dari Bagian 1 mengawasi jalur ini.COMMITmengunci seluruh rangkaian menjadi satu fakta permanen. Verifikasi penutup menunjukkan subtotal tiap baris dihitung kolom generated (85000, 120000, 70000 — total 275000), dan stok ketiga produk turun tepat sepuluh, lima, dan dua puluh.
Menolak Penjualan Melebihi Stok #
Skenario berikutnya menguji penjaga gerbang. Budi datang keesokan harinya dan memesan dua ratus botol Air Mineral — padahal stoknya seratus lima puluh. Kepala pesanan dan item-nya berhasil disisipkan, tetapi pengurangan stok membuat kolom stok bernilai negatif, dan di situlah CHECK (stok >= 0) menampik. Berkas ini sengaja berakhir dengan error; psql keluar dengan kode 3:
-- jual-lebih-stok.sql — percobaan menjual melebihi stok (overselling).
-- Air Mineral tersisa 150 botol, tetapi pesanan Budi meminta 200.
-- CHECK (stok >= 0) pada tabel produk menggagalkan UPDATE, dan seluruh
-- transaksi -- termasuk kepala pesanan yang sudah terlanjur di-INSERT --
-- dibatalkan oleh server.
BEGIN;
INSERT INTO pesanan (pelanggan_id, tanggal, status)
VALUES (1, DATE '2026-04-06', 'baru');
INSERT INTO pesanan_item (pesanan_id, produk_id, qty, harga_saat_transaksi)
SELECT p.id, pr.id, 200, pr.harga_jual
FROM pesanan p
JOIN produk pr ON pr.kode = 'PRD-007'
WHERE p.pelanggan_id = 1 AND p.tanggal = DATE '2026-04-06';
UPDATE produk pr
SET stok = pr.stok - i.qty
FROM pesanan_item i
WHERE i.pesanan_id = (SELECT id FROM pesanan
WHERE pelanggan_id = 1 AND tanggal = DATE '2026-04-06')
AND i.produk_id = pr.id;
COMMIT;Output yang Dihasilkan:
psql:jual-lebih-stok.sql:23: ERROR: new row for relation "produk" violates check constraint "produk_stok_check"
DETAIL: Failing row contains (7, PRD-007, Air Mineral 600ml, 2, 2500.00, 4000.00, -50).Pesan errornya layak dibaca per baris: constraint yang dilanggar adalah produk_stok_check, dan baris yang ditolak memuat nilai stok -50 — hasil 150 dikurangi 200. Pesanan Budi tidak masuk, dan tidak akan pernah masuk selama stok Air Mineral kurang dari yang diminta. Aplikasi tinggal menerjemahkan error ini menjadi pesan yang ramah untuk kasir: “stok Air Mineral tinggal 150, pesanan 200 tidak bisa diproses”.
Bukti Tidak Ada Jejak #
Penolakan di tengah transaksi meninggalkan pertanyaan penting: apa nasib kepala pesanan yang sudah terlanjur tersisipkan sebelum UPDATE gagal? Bukti berikut menjawabnya dengan tiga perhitungan sederhana:
-- pulih-atomic.sql — bukti atomisitas: transaksi overselling yang gagal
-- tidak meninggalkan satu pun jejak, meskipun INSERT kepala pesanan
-- berada di baris paling awal.
SELECT count(*) AS pesanan_enam_april
FROM pesanan
WHERE tanggal = DATE '2026-04-06';
SELECT count(*) AS item_enam_april
FROM pesanan_item i
JOIN pesanan p ON p.id = i.pesanan_id
WHERE p.tanggal = DATE '2026-04-06';
SELECT kode, nama, stok
FROM produk
WHERE kode = 'PRD-007';Output yang Dihasilkan:
pesanan_enam_april
--------------------
0
(1 row)
item_enam_april
-----------------
0
(1 row)
kode | nama | stok
---------+-------------------+------
PRD-007 | Air Mineral 600ml | 150
(1 row)Ketiganya nol dan utuh: tidak ada pesanan bertanggal 6 April, tidak ada itemnya, dan stok Air Mineral masih tepat 150. Ketika satu perintah di dalam blok transaksi gagal, PostgreSQL membatalkan seluruh blok tersebut — kepala pesanan yang sudah tersisip ikut digulung balik seolah tidak pernah ditulis. Inilah makna harfiah atomic: transaksi adalah unit yang tidak terbelah, jadi tidak ada hasil setengah jadi yang tersimpan.
Menyelesaikan Pesanan #
Pesanan Siti dari 5 April sudah diserahkan dan dibayar — statusnya perlu ditutup. Alur status pesanan di skema ini adalah satu jalan yang tertutup:
Daftar nilai yang sah hanya empat — baru, diproses, selesai, batal — dan transisi di luar diagram itu (misalnya batal kembali ke baru) adalah tanggung jawab aplikasi, bukan constraint. Yang dijaga database adalah pintu masuknya: pesanan baru selalu berstatus baru lewat nilai default, dan berkas berikut menutupnya menjadi selesai dengan penjaga transisi yang menarik:
-- selesaikan-pesanan.sql — pesanan diproses kasir lalu diselesaikan.
-- Klausa WHERE pada UPDATE sekaligus menjadi penjaga transisi status:
-- hanya pesanan berstatus 'baru' yang boleh berubah menjadi 'selesai'.
-- 1) tandai pesanan selesai
UPDATE pesanan
SET status = 'selesai'
WHERE pelanggan_id = 2 AND tanggal = DATE '2026-04-05' AND status = 'baru'
RETURNING id, status;
-- 2) total pesanan: agregasi di atas kolom generated subtotal
SELECT p.id AS pesanan,
pl.nama AS pelanggan,
p.status,
count(i.id) AS jumlah_item,
sum(i.subtotal) AS total
FROM pesanan p
JOIN pelanggan pl ON pl.id = p.pelanggan_id
JOIN pesanan_item i ON i.pesanan_id = p.id
WHERE p.tanggal = DATE '2026-04-05'
GROUP BY p.id, pl.nama, p.status
ORDER BY p.id;
-- 3) percobaan penyelesaian kedua: status sudah bukan 'baru',
-- sehingga UPDATE tidak menemukan baris apa pun
UPDATE pesanan
SET status = 'selesai'
WHERE pelanggan_id = 2 AND tanggal = DATE '2026-04-05' AND status = 'baru'
RETURNING id, status;Output yang Dihasilkan:
id | status
----+---------
1 | selesai
(1 row)
UPDATE 1
pesanan | pelanggan | status | jumlah_item | total
---------+-------------+---------+-------------+-----------
1 | Siti Rahayu | selesai | 3 | 275000.00
(1 row)
id | status
----+--------
(0 rows)
UPDATE 0Tiga hasil berturut-turut memperlihatkan penjaganya bekerja. UPDATE pertama menemukan pesanan Siti yang masih baru dan mengembalikannya sebagai selesai. Query total merangkum nilai pesanan: tiga item senilai 275000 — angka yang berasal dari agregasi sum(i.subtotal) di atas kolom generated, bukan dari kolom lain yang bisa bertentangan. UPDATE kedua menjalankan perintah yang persis sama, tetapi hasilnya UPDATE 0: kondisi status = 'baru' tidak lagi cocok untuk pesanan yang sudah selesai, sehingga tidak ada penutupan ganda. Pesanan yang sudah selesai tidak bisa diselesaikan lagi — aturan yang ditegakkan bukan oleh trigger atau constraint tambahan, melainkan oleh syarat pada WHERE itu sendiri; pola sederhana yang sering dipakai untuk transisi status di aplikasi nyata.
Alur penjualan kini tertutup: pesanan dibuat atomik dengan snapshot harga, penjualan melebihi stok ditolak, transaksi gagal tidak meninggalkan jejak, dan pesanan ditutup lewat transisi status yang terjaga. Di balik layar, kuartal yang baru berlalu meninggalkan dua belas pesanan di buku toko — dan pertanyaan pemilik toko bergeser dari “apa yang terjadi?” menjadi “apa yang harus saya lakukan berikutnya?”. Bagian Analisis Penjualan memuat riwayat kuartal itu dan mengolahnya menjadi jawaban.
Referensi #
- BEGIN — membuka blok transaksi.
- UPDATE
—
UPDATE ... FROMdanRETURNINGuntuk pengurangan stok set-based. - Constraints pada Tabel
—
CHECKsebagai penjaga anti-overselling. - INSERT
—
INSERT ... SELECTuntuk item pesanan dari tabel turunanVALUES.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-pesanan.sql
Unduh berkas-- buat-pesanan.sql — satu pesanan multi-item dalam SATU transaksi:
-- kepala pesanan, item dengan snapshot harga, dan pengurangan stok
-- terjadi bersama-sama atau tidak sama sekali.
BEGIN;
-- 1) kepala pesanan: Siti, 5 April 2026, status awal 'baru'
INSERT INTO pesanan (pelanggan_id, tanggal, status)
VALUES (2, DATE '2026-04-05', 'baru');
-- 2) item pesanan: harga DISALIN dari produk saat transaksi berlangsung.
-- Kolom harga_saat_transaksi adalah snapshot: harga di tabel produk
-- boleh berubah besok tanpa merusak nilai pesanan yang sudah lewat.
INSERT INTO pesanan_item (pesanan_id, produk_id, qty, harga_saat_transaksi)
SELECT p.id, pr.id, v.qty, pr.harga_jual
FROM (VALUES
('PRD-005', 10),
('PRD-002', 5),
('PRD-008', 20)
) AS v(kode_produk, qty)
JOIN pesanan p ON p.pelanggan_id = 2 AND p.tanggal = DATE '2026-04-05'
JOIN produk pr ON pr.kode = v.kode_produk;
-- 3) turunkan stok semua produk yang terjual
UPDATE produk pr
SET stok = pr.stok - i.qty
FROM pesanan_item i
WHERE i.pesanan_id = (SELECT id FROM pesanan
WHERE pelanggan_id = 2 AND tanggal = DATE '2026-04-05')
AND i.produk_id = pr.id;
COMMIT;
-- 4) verifikasi isi pesanan; subtotal dihitung kolom generated
SELECT p.id AS pesanan, pr.kode, pr.nama, i.qty,
i.harga_saat_transaksi, i.subtotal
FROM pesanan_item i
JOIN pesanan p ON p.id = i.pesanan_id
JOIN produk pr ON pr.id = i.produk_id
WHERE p.tanggal = DATE '2026-04-05'
ORDER BY pr.kode;
-- 5) stok ketiga produk setelah penjualan
SELECT pr.kode, pr.nama, pr.stok
FROM produk pr
WHERE pr.kode IN ('PRD-002', 'PRD-005', 'PRD-008')
ORDER BY pr.kode;
jual-lebih-stok.sql
Unduh berkas-- jual-lebih-stok.sql — percobaan menjual melebihi stok (overselling).
-- Air Mineral tersisa 150 botol, tetapi pesanan Budi meminta 200.
-- CHECK (stok >= 0) pada tabel produk menggagalkan UPDATE, dan seluruh
-- transaksi -- termasuk kepala pesanan yang sudah terlanjur di-INSERT --
-- dibatalkan oleh server.
BEGIN;
INSERT INTO pesanan (pelanggan_id, tanggal, status)
VALUES (1, DATE '2026-04-06', 'baru');
INSERT INTO pesanan_item (pesanan_id, produk_id, qty, harga_saat_transaksi)
SELECT p.id, pr.id, 200, pr.harga_jual
FROM pesanan p
JOIN produk pr ON pr.kode = 'PRD-007'
WHERE p.pelanggan_id = 1 AND p.tanggal = DATE '2026-04-06';
UPDATE produk pr
SET stok = pr.stok - i.qty
FROM pesanan_item i
WHERE i.pesanan_id = (SELECT id FROM pesanan
WHERE pelanggan_id = 1 AND tanggal = DATE '2026-04-06')
AND i.produk_id = pr.id;
COMMIT;
pulih-atomic.sql
Unduh berkas-- pulih-atomic.sql — bukti atomisitas: transaksi overselling yang gagal
-- tidak meninggalkan satu pun jejak, meskipun INSERT kepala pesanan
-- berada di baris paling awal.
SELECT count(*) AS pesanan_enam_april
FROM pesanan
WHERE tanggal = DATE '2026-04-06';
SELECT count(*) AS item_enam_april
FROM pesanan_item i
JOIN pesanan p ON p.id = i.pesanan_id
WHERE p.tanggal = DATE '2026-04-06';
SELECT kode, nama, stok
FROM produk
WHERE kode = 'PRD-007';
selesaikan-pesanan.sql
Unduh berkas-- selesaikan-pesanan.sql — pesanan diproses kasir lalu diselesaikan.
-- Klausa WHERE pada UPDATE sekaligus menjadi penjaga transisi status:
-- hanya pesanan berstatus 'baru' yang boleh berubah menjadi 'selesai'.
-- 1) tandai pesanan selesai
UPDATE pesanan
SET status = 'selesai'
WHERE pelanggan_id = 2 AND tanggal = DATE '2026-04-05' AND status = 'baru'
RETURNING id, status;
-- 2) total pesanan: agregasi di atas kolom generated subtotal
SELECT p.id AS pesanan,
pl.nama AS pelanggan,
p.status,
count(i.id) AS jumlah_item,
sum(i.subtotal) AS total
FROM pesanan p
JOIN pelanggan pl ON pl.id = p.pelanggan_id
JOIN pesanan_item i ON i.pesanan_id = p.id
WHERE p.tanggal = DATE '2026-04-05'
GROUP BY p.id, pl.nama, p.status
ORDER BY p.id;
-- 3) percobaan penyelesaian kedua: status sudah bukan 'baru',
-- sehingga UPDATE tidak menemukan baris apa pun
UPDATE pesanan
SET status = 'selesai'
WHERE pelanggan_id = 2 AND tanggal = DATE '2026-04-05' AND status = 'baru'
RETURNING id, status;
siapkan.sql
Unduh berkas-- siapkan.sql — fondasi Bagian 3: skema dari Bagian 1 + data master
-- dengan stok awal toko. Bagian ini dimaksudkan dapat dijalankan
-- sendiri dari database kosong.
CREATE TABLE kategori (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL UNIQUE
);
CREATE TABLE produk (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kode text NOT NULL UNIQUE,
nama text NOT NULL,
kategori_id integer NOT NULL REFERENCES kategori (id),
harga_beli numeric(12, 2) NOT NULL CHECK (harga_beli > 0),
harga_jual numeric(12, 2) NOT NULL CHECK (harga_jual > 0),
stok integer NOT NULL CHECK (stok >= 0)
);
CREATE TABLE pelanggan (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
nama text NOT NULL,
email text NOT NULL UNIQUE,
kota text NOT NULL
);
CREATE TABLE pesanan (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pelanggan_id integer NOT NULL REFERENCES pelanggan (id),
tanggal date NOT NULL,
status text NOT NULL DEFAULT 'baru'
CHECK (status IN ('baru', 'diproses', 'selesai', 'batal'))
);
CREATE TABLE pesanan_item (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pesanan_id integer NOT NULL REFERENCES pesanan (id) ON DELETE CASCADE,
produk_id integer NOT NULL REFERENCES produk (id),
qty integer NOT NULL CHECK (qty > 0),
harga_saat_transaksi numeric(12, 2) NOT NULL,
subtotal numeric(12, 2) GENERATED ALWAYS AS (qty * harga_saat_transaksi) STORED
);
CREATE INDEX idx_pesanan_tanggal ON pesanan (tanggal);
CREATE INDEX idx_pesanan_pelanggan ON pesanan (pelanggan_id);
CREATE INDEX idx_pesanan_item_pesanan ON pesanan_item (pesanan_id);
CREATE INDEX idx_pesanan_item_produk ON pesanan_item (produk_id);
INSERT INTO kategori (nama) VALUES
('Sembako'),
('Minuman'),
('Makanan'),
('Kebersihan');
INSERT INTO produk (kode, nama, kategori_id, harga_beli, harga_jual, stok) VALUES
('PRD-001', 'Beras Premium 5kg', 1, 61000.00, 68000.00, 40),
('PRD-002', 'Gula Pasir 1kg', 1, 15000.00, 17000.00, 60),
('PRD-003', 'Minyak Goreng 2L', 1, 32000.00, 36500.00, 35),
('PRD-004', 'Telur Ayam 1kg', 1, 27000.00, 31000.00, 25),
('PRD-005', 'Kopi Sachet Instan', 2, 9000.00, 12000.00, 80),
('PRD-006', 'Teh Celup 50 kantong', 2, 11000.00, 13500.00, 45),
('PRD-007', 'Air Mineral 600ml', 2, 2500.00, 4000.00, 150),
('PRD-008', 'Mie Instan Goreng', 3, 2800.00, 3500.00, 100),
('PRD-009', 'Biskuit Cokelat', 3, 7500.00, 9500.00, 30),
('PRD-010', 'Sabun Cuci Piring 780ml', 4, 16000.00, 19500.00, 22);
INSERT INTO pelanggan (nama, email, kota) VALUES
('Budi Santoso', '[email protected]', 'Bandung'),
('Siti Rahayu', '[email protected]', 'Bandung'),
('Andi Wijaya', '[email protected]', 'Jakarta'),
('Dewi Lestari', '[email protected]', 'Surabaya'),
('Rudi Hartono', '[email protected]', 'Bandung'),
('Maya Putri', '[email protected]', 'Jakarta');
-- papan stok awal: angka-angka ini yang diuji oleh skenario penjualan
SELECT kode, nama, stok
FROM produk
ORDER BY id;