Skema yang kita rancang di Bagian 1 baru berupa etalase kosong; toko baru bisa berjualan setelah katalog produk dan daftar pelanggan terisi dan terawat. Pada bagian ini kita memegang peran admin toko dalam pekerjaan hariannya: mendata delapan pelanggan dan sepuluh produk, menerima produk baru, menyesuaikan harga, memperbarui kota pengiriman pelanggan, sampai membersihkan data yang keliru. Di sepanjang jalan kita sengaja mencoba memasukkan data yang tidak valid — email ganda dan pesanan dari pelanggan yang tidak ada — untuk membuktikan constraint pada skema benar-benar bekerja, bukan sekadar hiasan deklarasi.

Menyiapkan Ulang Database #

Setiap bagian proyek ini mandiri: kita mulai dari database bersih yang dibangun ulang lewat database/skema.sql, sehingga hasil tiap bagian selalu bisa direproduksi tanpa bergantung pada bagian sebelumnya. Blok berikut menjalankan pernyataan yang sama dengan berkas skema dari Bagian 1:

sql
DROP DATABASE IF EXISTS penjualan_db;
CREATE DATABASE penjualan_db;
USE penjualan_db;

CREATE TABLE pelanggan (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(60) NOT NULL,
  email VARCHAR(80) NOT NULL UNIQUE,
  kota VARCHAR(30) NOT NULL
);

CREATE TABLE produk (
  id INT AUTO_INCREMENT PRIMARY KEY,
  sku VARCHAR(15) NOT NULL UNIQUE,
  nama VARCHAR(100) NOT NULL,
  kategori VARCHAR(30) NOT NULL,
  harga DECIMAL(10,2) NOT NULL DEFAULT 0.00,
  stok INT NOT NULL DEFAULT 0,
  CONSTRAINT chk_produk_stok CHECK (stok >= 0)
);

CREATE TABLE pesanan (
  no_pesanan INT AUTO_INCREMENT PRIMARY KEY,
  pelanggan_id INT NOT NULL,
  tanggal DATE NOT NULL,
  status ENUM('baru','dibayar','dikirim','selesai','dibatalkan') NOT NULL DEFAULT 'baru',
  CONSTRAINT fk_pesanan_pelanggan
    FOREIGN KEY (pelanggan_id) REFERENCES pelanggan(id)
);

CREATE TABLE detail_pesanan (
  pesanan_id INT NOT NULL,
  produk_id INT NOT NULL,
  jumlah INT NOT NULL,
  harga_satuan DECIMAL(10,2) NOT NULL,
  PRIMARY KEY (pesanan_id, produk_id),
  CONSTRAINT chk_detail_jumlah CHECK (jumlah > 0),
  CONSTRAINT fk_detail_pesanan
    FOREIGN KEY (pesanan_id) REFERENCES pesanan(no_pesanan),
  CONSTRAINT fk_detail_produk
    FOREIGN KEY (produk_id) REFERENCES produk(id)
);

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Database changed
Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Mengisi Data Awal #

Data contoh kita simpan di berkas database/data-awal.sql supaya seluruh bagian berikutnya memakai titik awal yang sama. Delapan pelanggan dan sepuluh produk semuanya fiktif, dengan nama-nama khas Indonesia dan domain contoh yang aman:

sql
INSERT INTO pelanggan (nama, email, kota) VALUES
  ('Budi Santoso', '[email protected]', 'Bandung'),
  ('Siti Rahmawati', '[email protected]', 'Jakarta'),
  ('Andi Wijaya', '[email protected]', 'Surabaya'),
  ('Dewi Lestari', '[email protected]', 'Yogyakarta'),
  ('Rina Marlina', '[email protected]', 'Bandung'),
  ('Tono Sutrisno', '[email protected]', 'Medan'),
  ('Sari Puspita', '[email protected]', 'Semarang'),
  ('Joko Prasetyo', '[email protected]', 'Denpasar');

INSERT INTO produk (sku, nama, kategori, harga, stok) VALUES
  ('TS-001', 'Kaos Polos Katun Combed 30s', 'Pakaian', 45000.00, 50),
  ('TS-002', 'Kemeja Flanel Lengan Panjang', 'Pakaian', 95000.00, 30),
  ('JA-001', 'Jaket Hoodie Zipper Unisex', 'Pakaian', 125000.00, 20),
  ('CP-001', 'Celana Chino Slim Fit', 'Pakaian', 85000.00, 35),
  ('SN-001', 'Sneakers Kanvas Klasik', 'Sepatu', 120000.00, 25),
  ('SN-002', 'Sandal Gunung Dewasa', 'Sepatu', 65000.00, 40),
  ('TA-001', 'Tas Ransel Laptop 15.6 inch', 'Tas', 145000.00, 15),
  ('TA-002', 'Totebag Kanvas Belajar', 'Tas', 35000.00, 60),
  ('AK-001', 'Topi Baseball Hitam', 'Aksesoris', 28000.00, 45),
  ('AK-002', 'Ikat Pinggang Kulit Sintetis', 'Aksesoris', 55000.00, 25);

Output yang Dihasilkan:

text
Query OK, 8 rows affected (0.00 sec)
Records: 8  Duplicates: 0  Warnings: 0

Query OK, 10 rows affected (0.00 sec)
Records: 10  Duplicates: 0  Warnings: 0

Dua keputusan kecil pada blok di atas patut dicatat. Pertama, INSERT multi-baris memakai satu pernyataan untuk delapan baris pelanggan dan sepuluh baris produk — lebih ringkas dan lebih cepat daripada INSERT terpisah per baris. Kedua, kategori produk sengaja dibiarkan sebagai kolom teks, bukan tabel tersendiri; untuk skala toko kecil ini cukup, dan bila suatu saat kategori perlu dikelola sendiri, kolom ini bisa dinaikkan menjadi tabel tanpa membongkar alur transaksi.

Isi yang baru ditulis kita baca kembali. Query diurutkan eksplisit lewat ORDER BY supaya urutan barisnya pasti:

sql
SELECT id, nama, email, kota
FROM pelanggan
ORDER BY id;

Output yang Dihasilkan:

text
+----+----------------+----------------------------+------------+
| id | nama           | email                      | kota       |
+----+----------------+----------------------------+------------+
|  1 | Budi Santoso   | [email protected]   | Bandung    |
|  2 | Siti Rahmawati | [email protected] | Jakarta    |
|  3 | Andi Wijaya    | [email protected]    | Surabaya   |
|  4 | Dewi Lestari   | [email protected]   | Yogyakarta |
|  5 | Rina Marlina   | [email protected]   | Bandung    |
|  6 | Tono Sutrisno  | [email protected]  | Medan      |
|  7 | Sari Puspita   | [email protected]   | Semarang   |
|  8 | Joko Prasetyo  | [email protected]  | Denpasar   |
+----+----------------+----------------------------+------------+
8 rows in set (0.00 sec)
sql
SELECT id, sku, nama, kategori, harga, stok
FROM produk
ORDER BY id;

Output yang Dihasilkan:

text
+----+--------+------------------------------+-----------+-----------+------+
| id | sku    | nama                         | kategori  | harga     | stok |
+----+--------+------------------------------+-----------+-----------+------+
|  1 | TS-001 | Kaos Polos Katun Combed 30s  | Pakaian   |  45000.00 |   50 |
|  2 | TS-002 | Kemeja Flanel Lengan Panjang | Pakaian   |  95000.00 |   30 |
|  3 | JA-001 | Jaket Hoodie Zipper Unisex   | Pakaian   | 125000.00 |   20 |
|  4 | CP-001 | Celana Chino Slim Fit        | Pakaian   |  85000.00 |   35 |
|  5 | SN-001 | Sneakers Kanvas Klasik       | Sepatu    | 120000.00 |   25 |
|  6 | SN-002 | Sandal Gunung Dewasa         | Sepatu    |  65000.00 |   40 |
|  7 | TA-001 | Tas Ransel Laptop 15.6 inch  | Tas       | 145000.00 |   15 |
|  8 | TA-002 | Totebag Kanvas Belajar       | Tas       |  35000.00 |   60 |
|  9 | AK-001 | Topi Baseball Hitam          | Aksesoris |  28000.00 |   45 |
| 10 | AK-002 | Ikat Pinggang Kulit Sintetis | Aksesoris |  55000.00 |   25 |
+----+--------+------------------------------+-----------+-----------+------+
10 rows in set (0.00 sec)

Delapan pelanggan tersebar dari Medan sampai Denpasar — kolom kota inilah yang nanti berguna saat menganalisis wilayah pasar di Bagian 4. Katalog produk menampilkan rentang harga dari totebag Rp35.000,00 sampai tas ransel Rp145.000,00 dengan stok yang paling banyak justru dipegang produk termurah — pola khas toko yang menjual banyak unit kecil.

Menambah dan Mengubah Data #

Kehidupan toko tidak berhenti di data awal. Produk baru masuk, harga berubah, dan pelanggan pindah kota. Tiga pernyataan berikut mensimulasikan ketiganya. Pertama, kacamata hitam masuk sebagai produk baru:

sql
INSERT INTO produk (sku, nama, kategori, harga, stok)
VALUES ('AK-003', 'Kacamata Hitam Polarized', 'Aksesoris', 85000.00, 20);

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

Kedua, harga sneaker naik karena harga bahan. Kolom harga menyimpan harga jual saat ini — harga inilah yang di-snapshot setiap kali pesanan dibuat di Bagian 3:

sql
UPDATE produk SET harga = 125000.00 WHERE sku = 'SN-001';
SELECT sku, nama, harga FROM produk WHERE sku = 'SN-001';

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

+--------+------------------------+-----------+
| sku    | nama                   | harga     |
+--------+------------------------+-----------+
| SN-001 | Sneakers Kanvas Klasik | 125000.00 |
+--------+------------------------+-----------+
1 row in set (0.00 sec)

Ketiga, Tono pindah dari Medan ke Jakarta sehingga kota pengirimannya diperbarui:

sql
UPDATE pelanggan SET kota = 'Jakarta' WHERE id = 6;
SELECT id, nama, kota FROM pelanggan WHERE id = 6;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

+----+---------------+---------+
| id | nama          | kota    |
+----+---------------+---------+
|  6 | Tono Sutrisno | Jakarta |
+----+---------------+---------+
1 row in set (0.00 sec)

Perhatikan laporan MySQL Rows matched: 1 Changed: 1 pada dua UPDATE di atas: satu baris cocok dengan WHERE dan satu baris berubah — dua angka yang sama berarti perubahan menjangkau tepat satu baris. Bila suatu UPDATE melaporkan Rows matched: 0, syarat WHERE-nya tidak menemukan sasaran; penanda gratis bahwa mungkin ada salah ketik pada SKU atau id.

Sebaliknya, penghapusan layak diperlakukan hati-hati. Admin salah menambahkan pelanggan Rudi dengan email yang keliru, dan akun itu belum pernah memesan apa pun:

sql
INSERT INTO pelanggan (nama, email, kota) VALUES
  ('Rudi Hartono', '[email protected]', 'Jakarta');
DELETE FROM pelanggan WHERE email = '[email protected]';
SELECT COUNT(*) AS jumlah_pelanggan FROM pelanggan;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)

+------------------+
| jumlah_pelanggan |
+------------------+
|                8 |
+------------------+
1 row in set (0.00 sec)

COUNT(*) tetap mengembalikan 8: baris Rudi sempat masuk lalu dihapus sebelum memiliki pesanan. Email exmaple.com memang sengaja dibuat salah ketik — domain yang keliru seperti ini adalah salah satu penyebab paling umum email pengiriman nyasar pada data nyata.

Menguji Constraint dengan Data Tidak Valid #

Bagian terpenting bagian ini adalah membuktikan constraint bekerja. Pertama kita coba mendaftarkan pelanggan baru dengan email yang sudah dipakai — [email protected] sudah dimiliki Budi Santoso:

sql
-- @error
INSERT INTO pelanggan (nama, email, kota) VALUES ('Budi Santoso Jr', '[email protected]', 'Bandung');

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1062 (23000): Duplicate entry '[email protected]' for key 'pelanggan.email'
Bye

MySQL menolak pernyataan itu dengan error duplikat pada pelanggan.email — nama constraint unique langsung menunjuk kolom mana yang dilanggar. Kedua, kita coba mencatat pesanan atas pelanggan bernomor 99 yang tidak pernah terdaftar:

sql
-- @error
INSERT INTO pesanan (pelanggan_id, tanggal, status) VALUES (99, '2026-03-01', 'baru');

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`penjualan_db`.`pesanan`, CONSTRAINT `fk_pesanan_pelanggan` FOREIGN KEY (`pelanggan_id`) REFERENCES `pelanggan` (`id`))
Bye

Error kedua datang dari foreign key fk_pesanan_pelanggan: baris anak (pesanan) menuntut nilai pelanggan_id yang benar-benar ada di tabel induk (pelanggan). Dua penolakan ini menegaskan pola penting: constraint adalah penjaga terakhir yang tetap bekerja meski aplikasi punya bug — data tidak valid tidak akan pernah lolos ke penyimpanan.

Referensi #

Katalog dan daftar pelanggan kini hidup dan terjaga constraint-nya. Namun toko online yang sejati diukur dari pesanan yang mengalir — pada Bagian 3 kita menjalankan alur intinya: membuat pesanan multi-produk yang mengurangi stok dalam satu transaksi database, menghitung totalnya, dan mengubah statusnya sepanjang daur hidup pesanan.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

database/data-awal.sql

Unduh berkas
sql
-- data-awal.sql — Proyek Database Penjualan
-- Tujuan: mengisi data contoh deterministik (10 produk dan 8 pelanggan
--   fiktif) sebagai titik awal konsisten untuk seluruh bagian proyek.
-- Cara pakai: jalankan setelah skema.sql:
--   mysql -u root -p penjualan_db < database/data-awal.sql

INSERT INTO pelanggan (nama, email, kota) VALUES
  ('Budi Santoso', '[email protected]', 'Bandung'),
  ('Siti Rahmawati', '[email protected]', 'Jakarta'),
  ('Andi Wijaya', '[email protected]', 'Surabaya'),
  ('Dewi Lestari', '[email protected]', 'Yogyakarta'),
  ('Rina Marlina', '[email protected]', 'Bandung'),
  ('Tono Sutrisno', '[email protected]', 'Medan'),
  ('Sari Puspita', '[email protected]', 'Semarang'),
  ('Joko Prasetyo', '[email protected]', 'Denpasar');

INSERT INTO produk (sku, nama, kategori, harga, stok) VALUES
  ('TS-001', 'Kaos Polos Katun Combed 30s', 'Pakaian', 45000.00, 50),
  ('TS-002', 'Kemeja Flanel Lengan Panjang', 'Pakaian', 95000.00, 30),
  ('JA-001', 'Jaket Hoodie Zipper Unisex', 'Pakaian', 125000.00, 20),
  ('CP-001', 'Celana Chino Slim Fit', 'Pakaian', 85000.00, 35),
  ('SN-001', 'Sneakers Kanvas Klasik', 'Sepatu', 120000.00, 25),
  ('SN-002', 'Sandal Gunung Dewasa', 'Sepatu', 65000.00, 40),
  ('TA-001', 'Tas Ransel Laptop 15.6 inch', 'Tas', 145000.00, 15),
  ('TA-002', 'Totebag Kanvas Belajar', 'Tas', 35000.00, 60),
  ('AK-001', 'Topi Baseball Hitam', 'Aksesoris', 28000.00, 45),
  ('AK-002', 'Ikat Pinggang Kulit Sintetis', 'Aksesoris', 55000.00, 25);

database/skema.sql

Unduh berkas
sql
-- skema.sql — Proyek Database Penjualan
-- Tujuan: membuat database penjualan_db beserta keempat tabelnya
--   (pelanggan, produk, pesanan, detail_pesanan) lengkap dengan primary key,
--   kolom unik email/sku, foreign key bernama, ENUM status, PK komposit,
--   dan check constraint stok/jumlah.
-- Cara pakai: mysql -u root -p < database/skema.sql

DROP DATABASE IF EXISTS penjualan_db;
CREATE DATABASE penjualan_db;
USE penjualan_db;

-- Pelanggan toko online; email unik sebagai identitas kontak.
CREATE TABLE pelanggan (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(60) NOT NULL,
  email VARCHAR(80) NOT NULL UNIQUE,
  kota VARCHAR(30) NOT NULL
);

-- Katalog produk; harga DECIMAL agar nilai rupiah pasti.
CREATE TABLE produk (
  id INT AUTO_INCREMENT PRIMARY KEY,
  sku VARCHAR(15) NOT NULL UNIQUE,
  nama VARCHAR(100) NOT NULL,
  kategori VARCHAR(30) NOT NULL,
  harga DECIMAL(10,2) NOT NULL DEFAULT 0.00,
  stok INT NOT NULL DEFAULT 0,
  CONSTRAINT chk_produk_stok CHECK (stok >= 0)
);

-- Satu baris per pesanan; status mengikuti daur hidup pesanan.
CREATE TABLE pesanan (
  no_pesanan INT AUTO_INCREMENT PRIMARY KEY,
  pelanggan_id INT NOT NULL,
  tanggal DATE NOT NULL,
  status ENUM('baru','dibayar','dikirim','selesai','dibatalkan') NOT NULL DEFAULT 'baru',
  CONSTRAINT fk_pesanan_pelanggan
    FOREIGN KEY (pelanggan_id) REFERENCES pelanggan(id)
);

-- Rincian produk per pesanan; harga_satuan adalah snapshot harga saat
-- pesanan dibuat, bukan harga terbaru di tabel produk.
CREATE TABLE detail_pesanan (
  pesanan_id INT NOT NULL,
  produk_id INT NOT NULL,
  jumlah INT NOT NULL,
  harga_satuan DECIMAL(10,2) NOT NULL,
  PRIMARY KEY (pesanan_id, produk_id),
  CONSTRAINT chk_detail_jumlah CHECK (jumlah > 0),
  CONSTRAINT fk_detail_pesanan
    FOREIGN KEY (pesanan_id) REFERENCES pesanan(no_pesanan),
  CONSTRAINT fk_detail_produk
    FOREIGN KEY (produk_id) REFERENCES produk(id)
);

queries/distribusi-status.sql

Unduh berkas
sql
-- distribusi-status.sql — Proyek Database Penjualan
-- Tujuan: menghitung banyaknya pesanan pada tiap status daur hidup
--   (baru, dibayar, dikirim, selesai, dibatalkan).
-- Cara pakai: mysql -u root -p penjualan_db < queries/distribusi-status.sql

SELECT status, COUNT(*) AS jumlah_pesanan
FROM pesanan
GROUP BY status
ORDER BY status;

queries/omzet-per-bulan.sql

Unduh berkas
sql
-- omzet-per-bulan.sql — Proyek Database Penjualan
-- Tujuan: menjumlahkan omzet per bulan dari rincian pesanan yang statusnya
--   sudah pasti menghasilkan uang (dibayar, dikirim, selesai) memakai
--   DATE_FORMAT atas tanggal literal dan harga_satuan snapshot.
-- Cara pakai: mysql -u root -p penjualan_db < queries/omzet-per-bulan.sql

SELECT DATE_FORMAT(p.tanggal, '%Y-%m') AS bulan,
       SUM(d.jumlah * d.harga_satuan) AS omzet
FROM pesanan p
JOIN detail_pesanan d ON d.pesanan_id = p.no_pesanan
WHERE p.status IN ('dibayar','dikirim','selesai')
GROUP BY DATE_FORMAT(p.tanggal, '%Y-%m')
ORDER BY bulan;

queries/pelanggan-teratas.sql

Unduh berkas
sql
-- pelanggan-teratas.sql — Proyek Database Penjualan
-- Tujuan: menampilkan pelanggan berdasarkan jumlah pesanan aktif dan total
--   belanja (jumlah x harga_satuan snapshot); pesanan dibatalkan dikecualikan.
--   Pelanggan tanpa transaksi tetap tampil dengan nilai nol.
-- Cara pakai: mysql -u root -p penjualan_db < queries/pelanggan-teratas.sql

SELECT pl.nama, pl.kota,
       COUNT(DISTINCT p.no_pesanan) AS jumlah_pesanan,
       COALESCE(SUM(d.jumlah * d.harga_satuan), 0) AS total_belanja
FROM pelanggan pl
LEFT JOIN pesanan p ON p.pelanggan_id = pl.id AND p.status <> 'dibatalkan'
LEFT JOIN detail_pesanan d ON d.pesanan_id = p.no_pesanan
GROUP BY pl.id, pl.nama, pl.kota
ORDER BY total_belanja DESC, pl.nama;

queries/produk-terlaris.sql

Unduh berkas
sql
-- produk-terlaris.sql — Proyek Database Penjualan
-- Tujuan: memeringkat produk berdasarkan total unit terjual (pesanan aktif,
--   termasuk yang berstatus baru) memakai window function RANK().
--   Produk tanpa penjualan tetap tampil dengan angka nol.
-- Cara pakai: mysql -u root -p penjualan_db < queries/produk-terlaris.sql

SELECT p.sku, p.nama,
       COALESCE(SUM(dj.jumlah), 0) AS total_terjual,
       RANK() OVER (ORDER BY COALESCE(SUM(dj.jumlah), 0) DESC) AS peringkat
FROM produk p
LEFT JOIN (
  SELECT d.produk_id, d.jumlah
  FROM detail_pesanan d
  JOIN pesanan pe ON pe.no_pesanan = d.pesanan_id
  WHERE pe.status <> 'dibatalkan'
) dj ON dj.produk_id = p.id
GROUP BY p.id, p.sku, p.nama
ORDER BY total_terjual DESC, p.sku;

tests/pemeriksaan.sql

Unduh berkas
sql
-- pemeriksaan.sql — Proyek Database Penjualan
-- Tujuan: pemeriksaan akhir seluruh fitur proyek (pembuatan pesanan atomik,
--   penghitungan total, perubahan status, pembatalan yang memulihkan stok,
--   omzet per bulan, produk terlaris, dan distribusi status).
--   Berkas ini MANDIRI: membangun skenario uji kecilnya sendiri,
--   lalu membersihkan database setelah selesai.
-- Cara pakai: mysql -u root -p < tests/pemeriksaan.sql
-- Ekspektasi tiap hasil ditulis sebagai komentar di atas query terkait.

DROP DATABASE IF EXISTS penjualan_db;
CREATE DATABASE penjualan_db;
USE penjualan_db;

CREATE TABLE pelanggan (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(60) NOT NULL,
  email VARCHAR(80) NOT NULL UNIQUE,
  kota VARCHAR(30) NOT NULL
);
CREATE TABLE produk (
  id INT AUTO_INCREMENT PRIMARY KEY,
  sku VARCHAR(15) NOT NULL UNIQUE,
  nama VARCHAR(100) NOT NULL,
  kategori VARCHAR(30) NOT NULL,
  harga DECIMAL(10,2) NOT NULL DEFAULT 0.00,
  stok INT NOT NULL DEFAULT 0,
  CONSTRAINT chk_produk_stok CHECK (stok >= 0)
);
CREATE TABLE pesanan (
  no_pesanan INT AUTO_INCREMENT PRIMARY KEY,
  pelanggan_id INT NOT NULL,
  tanggal DATE NOT NULL,
  status ENUM('baru','dibayar','dikirim','selesai','dibatalkan') NOT NULL DEFAULT 'baru',
  CONSTRAINT fk_pesanan_pelanggan
    FOREIGN KEY (pelanggan_id) REFERENCES pelanggan(id)
);
CREATE TABLE detail_pesanan (
  pesanan_id INT NOT NULL,
  produk_id INT NOT NULL,
  jumlah INT NOT NULL,
  harga_satuan DECIMAL(10,2) NOT NULL,
  PRIMARY KEY (pesanan_id, produk_id),
  CONSTRAINT chk_detail_jumlah CHECK (jumlah > 0),
  CONSTRAINT fk_detail_pesanan
    FOREIGN KEY (pesanan_id) REFERENCES pesanan(no_pesanan),
  CONSTRAINT fk_detail_produk
    FOREIGN KEY (produk_id) REFERENCES produk(id)
);

INSERT INTO pelanggan (nama, email, kota) VALUES
  ('Budi Santoso', '[email protected]', 'Bandung'),
  ('Siti Rahmawati', '[email protected]', 'Jakarta'),
  ('Andi Wijaya', '[email protected]', 'Surabaya');
INSERT INTO produk (sku, nama, kategori, harga, stok) VALUES
  ('TS-001', 'Kaos Polos Katun Combed 30s', 'Pakaian', 45000.00, 20),
  ('TA-002', 'Totebag Kanvas Belajar', 'Tas', 35000.00, 15),
  ('AK-001', 'Topi Baseball Hitam', 'Aksesoris', 28000.00, 10);

-- Pemeriksaan 1: pesanan dibuat atomik - rincian masuk, stok berkurang.
START TRANSACTION;
INSERT INTO pesanan (pelanggan_id, tanggal, status)
  VALUES (2, '2026-01-10', 'baru');
INSERT INTO detail_pesanan (pesanan_id, produk_id, jumlah, harga_satuan) VALUES
  (LAST_INSERT_ID(), 1, 2, 45000.00),
  (LAST_INSERT_ID(), 3, 1, 28000.00);
UPDATE produk SET stok = stok - 2 WHERE id = 1;
UPDATE produk SET stok = stok - 1 WHERE id = 3;
COMMIT;
-- Ekspektasi: TS-001 stok 18, AK-001 stok 9
SELECT sku, stok FROM produk WHERE id IN (1, 3) ORDER BY id;

-- Pemeriksaan 2: total pesanan dihitung dari rincian (SUM jumlah x harga).
-- Ekspektasi: pesanan 1 total 118000.00
SELECT d.pesanan_id, SUM(d.jumlah * d.harga_satuan) AS total
FROM detail_pesanan d
GROUP BY d.pesanan_id
ORDER BY d.pesanan_id;

-- Pemeriksaan 3: status pesanan berubah dari baru menjadi dibayar.
UPDATE pesanan SET status = 'dibayar' WHERE no_pesanan = 1 AND status = 'baru';
-- Ekspektasi: pesanan 1 status dibayar
SELECT no_pesanan, status FROM pesanan WHERE no_pesanan = 1;

-- Pemeriksaan 4: pembatalan pesanan mengembalikan status dan stok.
START TRANSACTION;
INSERT INTO pesanan (pelanggan_id, tanggal, status)
  VALUES (3, '2026-01-15', 'baru');
INSERT INTO detail_pesanan (pesanan_id, produk_id, jumlah, harga_satuan)
  VALUES (2, 2, 1, 35000.00);
UPDATE produk SET stok = stok - 1 WHERE id = 2;
COMMIT;
START TRANSACTION;
UPDATE pesanan SET status = 'dibatalkan' WHERE no_pesanan = 2 AND status = 'baru';
UPDATE produk SET stok = stok + 1 WHERE id = 2;
COMMIT;
-- Ekspektasi: pesanan 2 dibatalkan, TA-002 stok pulih ke 15
SELECT no_pesanan, status FROM pesanan WHERE no_pesanan = 2;
SELECT sku, stok FROM produk WHERE id = 2;

-- Pemeriksaan 5: pesanan ketiga (Budi) selesai di bulan Februari.
START TRANSACTION;
INSERT INTO pesanan (pelanggan_id, tanggal, status)
  VALUES (1, '2026-02-05', 'baru');
INSERT INTO detail_pesanan (pesanan_id, produk_id, jumlah, harga_satuan)
  VALUES (3, 1, 1, 45000.00);
UPDATE produk SET stok = stok - 1 WHERE id = 1;
COMMIT;
UPDATE pesanan SET status = 'selesai' WHERE no_pesanan = 3 AND status = 'baru';
-- Ekspektasi: pesanan 3 selesai
SELECT no_pesanan, status FROM pesanan WHERE no_pesanan = 3;

-- Pemeriksaan 6: omzet per bulan hanya dari pesanan yang menghasilkan uang.
-- Ekspektasi: 2026-01 omzet 118000.00 (pesanan 2 dibatalkan dikecualikan),
--   2026-02 omzet 45000.00
SELECT DATE_FORMAT(p.tanggal, '%Y-%m') AS bulan,
       SUM(d.jumlah * d.harga_satuan) AS omzet
FROM pesanan p
JOIN detail_pesanan d ON d.pesanan_id = p.no_pesanan
WHERE p.status IN ('dibayar','dikirim','selesai')
GROUP BY DATE_FORMAT(p.tanggal, '%Y-%m')
ORDER BY bulan;

-- Pemeriksaan 7: produk terlaris dengan RANK(); pesanan dibatalkan dikecualikan.
-- Ekspektasi: TS-001 3 unit peringkat 1, AK-001 1 unit peringkat 2,
--   TA-002 0 unit peringkat 3
SELECT p.sku,
       COALESCE(SUM(dj.jumlah), 0) AS total_terjual,
       RANK() OVER (ORDER BY COALESCE(SUM(dj.jumlah), 0) DESC) AS peringkat
FROM produk p
LEFT JOIN (
  SELECT d.produk_id, d.jumlah
  FROM detail_pesanan d
  JOIN pesanan pe ON pe.no_pesanan = d.pesanan_id
  WHERE pe.status <> 'dibatalkan'
) dj ON dj.produk_id = p.id
GROUP BY p.id, p.sku
ORDER BY total_terjual DESC, p.sku;

-- Pemeriksaan 8: distribusi status seluruh pesanan.
-- Ekspektasi: dibatalkan 1, dibayar 1, selesai 1
SELECT status, COUNT(*) AS jumlah_pesanan
FROM pesanan
GROUP BY status
ORDER BY status;

DROP DATABASE penjualan_db;