Bagian 1: Analisa Brief Projek dari Client
Kalau kalian freelancer Laravel, pasti familiar dengan situasi ini:
Client WhatsApp: "Mas, saya mau bikin aplikasi inventory. Ada fitur input barang, laporan, sama multi-user. Bisa kan? Berapa harganya?"
Terus kalian bingung. Itu doang? Inventory itu scope-nya bisa sekecil spreadsheet glorified, bisa sebesar ERP mini. "Laporan" itu laporan apa? "Multi-user" itu berapa user? Ada role-based access?
Brief dari client itu hampir selalu ambigu. Dan dari pengalaman saya, 80% masalah di tengah projek terjadi karena scope nggak clear di awal.
Di sinilah AI jadi game changer. Bukan buat ngerjain projeknya, tapi buat bantu kalian breakdown brief yang ambigu jadi sesuatu yang actionable.
Masalah Klasik Brief Client
Sebelum bahas solusi, let's acknowledge masalahnya dulu:
Brief terlalu high-level Client bilang "aplikasi inventory" — tapi itu bisa berarti 100 hal berbeda tergantung bisnis mereka.
Banyak asumsi yang nggak tertulis Client assume kalian "pasti tau" mereka butuh export Excel. Padahal nggak pernah disebutin.
Scope creep tersembunyi "Oh iya, sama tambahin notifikasi WhatsApp ya" — di tengah projek, padahal nggak ada di brief awal.
Estimasi meleset Karena scope nggak jelas, estimasi waktu dan harga jadi gambling. Sering rugi waktu dan tenaga.
Gimana AI Membantu
AI (dalam hal ini saya pakai Claude, bisa juga via Claude Code di VS Code) bisa bantu:
- Breakdown brief jadi fitur-fitur spesifik
- Generate pertanyaan yang harus ditanyakan ke client
- Spot potential scope creep sebelum terjadi
- Estimasi complexity per fitur
Intinya, AI bantu kalian berpikir lebih sistematis dan nggak kelewatan hal-hal penting.
Contoh Skenario Nyata
Let's use real example. Client kasih brief:
"Saya mau bikin aplikasi manajemen inventory untuk toko saya. Ada fitur input barang, laporan, sama multi-user."
Cuma itu. 15 kata. Dan dari 15 kata ini, kalian harus kasih estimasi harga.
Dulu, saya bakal langsung mikir sendiri, bikin asumsi sendiri, kasih harga, terus nanti ribut di tengah jalan.
Sekarang, saya lempar ke Claude dulu.
Prompt yang Saya Gunakan
Saya freelancer Laravel dan dapat brief projek dari client:
"Saya mau bikin aplikasi manajemen inventory untuk toko saya. Ada fitur input barang, laporan, sama multi-user."
Tolong bantu saya:
1. Breakdown brief ini jadi list fitur yang lebih detail dan kemungkinan sub-fitur
2. Identifikasi pertanyaan penting yang HARUS saya tanyakan ke client sebelum mulai
3. Spot potential scope creep — fitur yang kemungkinan diminta later tapi nggak disebut sekarang
4. Estimasi complexity (Low/Medium/High) untuk setiap fitur utama
Context: Ini untuk toko retail fisik (bukan e-commerce).
Format dalam struktur yang rapi dan mudah dibaca.
Output dari Claude
Ini contoh output yang saya dapat:
1. BREAKDOWN FITUR DETAIL
A. Manajemen Barang (Input Barang)
- CRUD Produk (nama, SKU, deskripsi, foto)
- Kategori produk (kemungkinan hierarki: Kategori > Sub-kategori)
- Harga beli dan harga jual
- Stok awal dan stok current
- Minimum stok alert
- Barcode/SKU generator atau manual input
- Import bulk data (dari Excel existing)
B. Transaksi Inventory
- Transaksi Masuk (pembelian/restock dari supplier)
- Transaksi Keluar (penjualan atau penggunaan internal)
- Stock opname / adjustment
- Retur barang
- History transaksi per barang
C. Laporan
- Laporan stok saat ini
- Laporan barang masuk per periode
- Laporan barang keluar per periode
- Laporan profit margin
- Laporan barang fast-moving vs slow-moving
- Export ke Excel/PDF
D. Multi-User
- Role-based access (Admin, Staff, Viewer)
- Activity log per user
- Approval workflow? (untuk transaksi tertentu)
- Multi-outlet? Atau single outlet?
2. PERTANYAAN PENTING UNTUK CLIENT
Tentang Bisnis:
- Berapa jumlah SKU/produk yang akan dikelola? (50? 500? 5000?)
- Apakah ada multiple outlet/gudang atau single location?
- Apakah sudah ada data existing yang perlu di-migrate?
Tentang Fitur Input Barang:
- Apakah perlu barcode scanning?
- Apakah produk punya varian (size, color)?
- Apakah perlu track batch/expired date?
Tentang Transaksi:
- Apakah akan ada integrasi dengan POS (Point of Sale)?
- Siapa yang input transaksi? Semua staff atau admin saja?
- Apakah perlu approval untuk transaksi tertentu?
Tentang Laporan:
- Laporan apa yang paling sering dibutuhkan?
- Siapa yang akan lihat laporan?
- Apakah perlu laporan otomatis (scheduled email)?
Tentang User:
- Berapa user yang akan mengakses?
- Role apa saja yang dibutuhkan?
- Apakah perlu audit trail (siapa melakukan apa)?
Tentang Teknis:
- Akses via web saja atau butuh mobile app?
- Cloud atau install di server lokal?
- Budget range?
3. POTENTIAL SCOPE CREEP
⚠️ Fitur yang kemungkinan besar diminta nanti tapi nggak disebutkan sekarang:
| Fitur | Probabilitas | Impact |
|---|---|---|
| Integrasi POS/Kasir | Tinggi | Besar — bisa jadi projek terpisah |
| Mobile app untuk stock check | Tinggi | Sedang — perlu development terpisah |
| Notifikasi WhatsApp/Email untuk low stock | Tinggi | Kecil-Sedang |
| Multi-outlet/gudang | Sedang | Besar — arsitektur berbeda |
| Integrasi dengan supplier | Sedang | Sedang |
| Barcode printing | Sedang | Kecil |
| Customer database | Sedang | Sedang |
| Hutang/piutang | Sedang | Sedang-Besar |
| Dashboard analytics | Tinggi | Sedang |
Rekomendasi: Tanyakan eksplisit apakah fitur-fitur ini dibutuhkan di phase 1 atau bisa di phase berikutnya.
4. ESTIMASI COMPLEXITY
| Fitur | Complexity | Estimasi Waktu* | Notes |
|---|---|---|---|
| CRUD Produk + Kategori | Low | 2-3 hari | Standard CRUD |
| Upload foto produk | Low | 1 hari | Storage handling |
| Transaksi Masuk/Keluar | Medium | 3-4 hari | Business logic stok |
| Stock opname | Medium | 2-3 hari | Adjustment logic |
| Laporan basic | Low-Medium | 3-4 hari | Query + Export |
| Laporan dengan grafik | Medium | 2-3 hari | Chart library |
| Multi-user + RBAC | Medium | 2-3 hari | Auth + Permission |
| Activity log | Low | 1 hari | Spatie Activitylog |
| Low stock alert | Low | 1 hari | Notification |
| Import data Excel | Medium | 2 hari | Mapping + Validation |
- Estimasi untuk developer Laravel berpengalaman
Total Rough Estimate: 20-30 hari kerja untuk MVP yang solid
Apa yang Saya Lakukan dengan Output Ini
Output dari Claude ini bukan untuk langsung dikirim ke client. Ini untuk saya olah:
1. Bikin List Pertanyaan
Dari pertanyaan yang di-generate, saya pilih yang paling critical. Nggak semua perlu ditanya — nanti client overwhelmed. Saya pilih 8-10 pertanyaan prioritas:
Halo Pak/Bu,
Terima kasih untuk brief proyeknya. Sebelum saya buatkan estimasi,
ada beberapa pertanyaan untuk memastikan saya memahami kebutuhannya:
1. Kira-kira berapa jumlah produk yang akan dikelola?
2. Apakah ada data existing yang perlu dipindahkan ke sistem baru?
3. Untuk multi-user, berapa orang yang akan mengakses? Dan apakah
perlu pembatasan akses (misal: staff hanya bisa input, admin bisa
lihat laporan)?
4. Untuk laporan, laporan apa yang paling sering dibutuhkan?
5. Apakah perlu fitur barcode atau cukup input manual?
6. Apakah nantinya perlu integrasi dengan sistem kasir (POS)?
7. Akses via laptop/komputer saja atau perlu bisa dari HP juga?
8. Apakah ada timeline tertentu yang diharapkan?
Setelah saya dapat jawaban ini, saya akan buatkan proposal lengkap
dengan fitur, timeline, dan estimasi biaya.
2. Prepare Scope Document
Dari breakdown fitur, saya bikin draft scope document. Nanti setelah client jawab pertanyaan, tinggal adjust.
3. Identify Red Flags
Dari potential scope creep, saya jadi aware. Kalau client mulai minta "tambahin POS" di tengah jalan, saya udah siap bilang itu di luar scope awal dan butuh additional cost.
4. Estimasi Lebih Akurat
Daripada nebak-nebak "kayaknya 2 minggu deh", sekarang saya punya breakdown per fitur. Lebih defensible kalau client nego.
Tips Menggunakan AI untuk Analisa Brief
Selalu kasih context
Jangan cuma copy-paste brief client. Tambahin context:
- Jenis bisnis client (retail, F&B, manufacturing)
- Scale (UMKM, medium, enterprise)
- Technical constraints kalau ada
Validate dengan experience
AI nggak tau client kalian. Output AI adalah starting point. Kalian yang filter mana yang relevant.
Jangan langsung percaya estimasi waktu
Estimasi waktu dari AI based on "average". Kalian yang tau speed kalian sendiri. Adjust accordingly.
Simpan prompt yang works
Kalau udah nemu prompt yang hasilnya bagus, simpan. Build library of prompts untuk berbagai situasi.
Prompt Variations yang Bisa Dicoba
Untuk brief yang lebih teknikal:
Brief projek: [paste brief]
Sebagai senior Laravel developer, analyze brief ini dan identifikasi:
1. Technical challenges yang mungkin muncul
2. Third-party packages yang kemungkinan dibutuhkan
3. Architecture decisions yang perlu diputuskan di awal
4. Integration points dengan sistem lain
Untuk compare dengan projek serupa:
Brief projek: [paste brief]
Bandingkan dengan aplikasi inventory yang umum di pasaran (seperti Jurnal,
Moka, iReap). Fitur apa yang biasanya ada di aplikasi tersebut yang tidak
disebutkan di brief ini? Mana yang likely dibutuhkan client?
Untuk estimasi pricing:
Berdasarkan breakdown fitur berikut:
[paste breakdown]
Dengan asumsi:
- Rate: Rp 500.000/hari
- Developer: 1 orang
- Include testing dan bug fixing
Buatkan estimasi harga dengan range (minimum - maximum) dan jelaskan
apa yang mempengaruhi range tersebut.
Common Pitfalls
Jangan terlalu rely on AI untuk final decision
AI nggak tau:
- Relationship kalian sama client
- Budget real client
- Timeline yang realistic untuk kalian specifically
- Red flags dari komunikasi client
Ini tetap judgment call kalian.
Jangan kirim output AI langsung ke client
Output AI perlu di-humanize. Bahasa dan tone perlu disesuaikan dengan client. Dan ada info yang untuk internal consumption aja.
Jangan skip validasi
AI bisa miss things atau suggest something that doesn't make sense untuk context specific. Always review.
Checklist Sebelum Lanjut ke Tahap Design
Setelah analisa brief dengan bantuan AI, pastikan:
- [ ] Brief sudah di-breakdown jadi fitur-fitur spesifik
- [ ] Pertanyaan clarification sudah dikirim dan dijawab client
- [ ] Potential scope creep sudah di-identify dan di-communicate
- [ ] Estimasi waktu dan harga sudah punya dasar yang jelas
- [ ] Scope document (minimal draft) sudah ada
Kalau semua kecentang, kalian ready untuk tahap selanjutnya: design database dengan ERD.
Bagian 2: Menyusun ERD (Entity Relationship Diagram)
Oke, brief udah di-breakdown, pertanyaan udah dijawab client, scope udah clear. Sekarang saatnya design database.
Dan ini tahap yang kalau salah di awal, sakitnya berasa di tengah sampai akhir projek.
Saya pernah ngalamin sendiri. Projek udah jalan 2 minggu, tiba-tiba realize struktur tabel nggak support fitur yang diminta. Harus refactor migrations, update semua relationships, fix semua queries. Makan waktu 3 hari yang seharusnya nggak perlu.
Sekarang, sebelum nulis satu baris migration pun, saya selalu design ERD dulu. Dan AI significantly speed up proses ini.
Kenapa ERD Penting
Visualisasi sebelum implementasi ERD bikin kalian "lihat" struktur data sebelum coding. Lebih gampang spot masalah di diagram daripada di code.
Diskusi dengan client atau tim Kalau projek melibatkan tim atau client yang teknikal, ERD jadi communication tool yang bagus.
Prevent costly refactoring Fix di tahap design = menit. Fix di tahap production = hari atau minggu.
Documentation ERD jadi dokumentasi yang berguna untuk maintenance later.
Gimana AI Membantu Design ERD
AI bisa:
- Generate initial ERD dari requirements
- Suggest relasi yang mungkin kelewatan
- Identify fields yang commonly needed tapi belum disebut
- Generate diagram dalam format Mermaid (bisa di-render)
- Langsung generate Laravel migrations dari ERD
Workflow saya sekarang:
- Kasih requirements ke AI → dapat ERD draft
- Review dan adjust
- Minta AI generate migrations
- Review migrations
- Run dan test
Contoh: Design ERD untuk Inventory App
Melanjutkan dari brief di Bagian 1, sekarang kita design database-nya.
Requirements yang sudah clear setelah tanya-jawab dengan client:
- Multi-user dengan role (admin, staff)
- CRUD produk dengan kategori (single level, nggak nested)
- Track harga beli dan harga jual
- Transaksi masuk (pembelian dari supplier)
- Transaksi keluar (penjualan)
- Laporan stok dan profit
- Activity log
Prompt untuk Generate ERD
Saya akan membangun aplikasi inventory management dengan Laravel 11 dan MySQL.
Requirements:
1. Multi-user dengan roles (admin: full access, staff: input transaksi saja)
2. Produk: nama, SKU (unique), deskripsi, foto, kategori, harga beli, harga jual, stok
3. Kategori produk (single level, tidak nested)
4. Supplier: untuk track pembelian dari mana
5. Transaksi Masuk: pembelian produk dari supplier, bisa multiple items per transaksi
6. Transaksi Keluar: penjualan, bisa multiple items per transaksi
7. Setiap transaksi harus track siapa yang input (user)
8. Activity log untuk audit trail
Tolong buatkan:
1. ERD dalam format Mermaid diagram
2. List semua tabel dengan kolom, tipe data, dan constraints
3. Penjelasan setiap relasi antar tabel
4. Indexes yang direkomendasikan untuk performa query yang umum
Gunakan naming convention Laravel (snake_case, plural untuk nama tabel).
Output ERD dari Claude
1. MERMAID ERD DIAGRAM
erDiagram
users ||--o{ transactions_in : creates
users ||--o{ transactions_out : creates
users ||--o{ activity_logs : performs
roles ||--o{ users : has
categories ||--o{ products : contains
products ||--o{ transaction_in_items : included_in
products ||--o{ transaction_out_items : included_in
suppliers ||--o{ transactions_in : supplies
transactions_in ||--|{ transaction_in_items : has
transactions_out ||--|{ transaction_out_items : has
users {
bigint id PK
bigint role_id FK
string name
string email UK
string password
string phone
boolean is_active
timestamp email_verified_at
timestamps timestamps
}
roles {
bigint id PK
string name UK
string display_name
timestamps timestamps
}
categories {
bigint id PK
string name
string slug UK
text description
timestamps timestamps
}
products {
bigint id PK
bigint category_id FK
string name
string sku UK
text description
string image
decimal buy_price
decimal sell_price
integer stock
integer min_stock
boolean is_active
timestamps timestamps
softDeletes deleted_at
}
suppliers {
bigint id PK
string name
string email
string phone
text address
boolean is_active
timestamps timestamps
softDeletes deleted_at
}
transactions_in {
bigint id PK
bigint supplier_id FK
bigint user_id FK
string invoice_number UK
date transaction_date
decimal total_amount
text notes
timestamps timestamps
softDeletes deleted_at
}
transaction_in_items {
bigint id PK
bigint transaction_in_id FK
bigint product_id FK
integer quantity
decimal price
decimal subtotal
timestamps timestamps
}
transactions_out {
bigint id PK
bigint user_id FK
string invoice_number UK
date transaction_date
decimal subtotal
decimal discount
decimal total_amount
text notes
timestamps timestamps
softDeletes deleted_at
}
transaction_out_items {
bigint id PK
bigint transaction_out_id FK
bigint product_id FK
integer quantity
decimal price
decimal subtotal
timestamps timestamps
}
activity_logs {
bigint id PK
bigint user_id FK
string action
string model_type
bigint model_id
json old_values
json new_values
string ip_address
timestamps timestamps
}

2. DETAIL TABEL
Tabel: users
| Kolom | Tipe | Constraints | Keterangan |
|---|---|---|---|
| id | BIGINT UNSIGNED | PRIMARY KEY, AUTO_INCREMENT | |
| role_id | BIGINT UNSIGNED | FOREIGN KEY → roles.id | |
| name | VARCHAR(255) | NOT NULL | |
| VARCHAR(255) | NOT NULL, UNIQUE | ||
| password | VARCHAR(255) | NOT NULL | |
| phone | VARCHAR(20) | NULLABLE | |
| is_active | BOOLEAN | DEFAULT true | Soft disable tanpa delete |
| email_verified_at | TIMESTAMP | NULLABLE | |
| created_at | TIMESTAMP | ||
| updated_at | TIMESTAMP |
Tabel: roles
| Kolom | Tipe | Constraints | Keterangan |
|---|---|---|---|
| id | BIGINT UNSIGNED | PRIMARY KEY, AUTO_INCREMENT | |
| name | VARCHAR(50) | NOT NULL, UNIQUE | admin, staff |
| display_name | VARCHAR(100) | NOT NULL | Admin, Staff |
| created_at | TIMESTAMP | ||
| updated_at | TIMESTAMP |
Tabel: categories
| Kolom | Tipe | Constraints | Keterangan |
|---|---|---|---|
| id | BIGINT UNSIGNED | PRIMARY KEY, AUTO_INCREMENT | |
| name | VARCHAR(255) | NOT NULL | |
| slug | VARCHAR(255) | NOT NULL, UNIQUE | URL-friendly |
| description | TEXT | NULLABLE | |
| created_at | TIMESTAMP | ||
| updated_at | TIMESTAMP |
Tabel: products
| Kolom | Tipe | Constraints | Keterangan |
|---|---|---|---|
| id | BIGINT UNSIGNED | PRIMARY KEY, AUTO_INCREMENT | |
| category_id | BIGINT UNSIGNED | FOREIGN KEY → categories.id | |
| name | VARCHAR(255) | NOT NULL | |
| sku | VARCHAR(100) | NOT NULL, UNIQUE | Stock Keeping Unit |
| description | TEXT | NULLABLE | |
| image | VARCHAR(255) | NULLABLE | Path ke storage |
| buy_price | DECIMAL(15,2) | NOT NULL, DEFAULT 0 | Harga beli |
| sell_price | DECIMAL(15,2) | NOT NULL, DEFAULT 0 | Harga jual |
| stock | INTEGER | NOT NULL, DEFAULT 0 | Current stock |
| min_stock | INTEGER | NOT NULL, DEFAULT 0 | Alert threshold |
| is_active | BOOLEAN | DEFAULT true | |
| created_at | TIMESTAMP | ||
| updated_at | TIMESTAMP | ||
| deleted_at | TIMESTAMP | NULLABLE | Soft delete |
Tabel: suppliers
| Kolom | Tipe | Constraints | Keterangan |
|---|---|---|---|
| id | BIGINT UNSIGNED | PRIMARY KEY, AUTO_INCREMENT | |
| name | VARCHAR(255) | NOT NULL | |
| VARCHAR(255) | NULLABLE | ||
| phone | VARCHAR(20) | NULLABLE | |
| address | TEXT | NULLABLE | |
| is_active | BOOLEAN | DEFAULT true | |
| created_at | TIMESTAMP | ||
| updated_at | TIMESTAMP | ||
| deleted_at | TIMESTAMP | NULLABLE | Soft delete |
Tabel: transactions_in (Pembelian/Restock)
| Kolom | Tipe | Constraints | Keterangan |
|---|---|---|---|
| id | BIGINT UNSIGNED | PRIMARY KEY, AUTO_INCREMENT | |
| supplier_id | BIGINT UNSIGNED | FOREIGN KEY → suppliers.id | |
| user_id | BIGINT UNSIGNED | FOREIGN KEY → users.id | Yang input |
| invoice_number | VARCHAR(50) | NOT NULL, UNIQUE | Auto-generate |
| transaction_date | DATE | NOT NULL | |
| total_amount | DECIMAL(15,2) | NOT NULL | Sum of items |
| notes | TEXT | NULLABLE | |
| created_at | TIMESTAMP | ||
| updated_at | TIMESTAMP | ||
| deleted_at | TIMESTAMP | NULLABLE |
Tabel: transaction_in_items
| Kolom | Tipe | Constraints | Keterangan |
|---|---|---|---|
| id | BIGINT UNSIGNED | PRIMARY KEY, AUTO_INCREMENT | |
| transaction_in_id | BIGINT UNSIGNED | FOREIGN KEY → transactions_in.id | |
| product_id | BIGINT UNSIGNED | FOREIGN KEY → products.id | |
| quantity | INTEGER | NOT NULL | Jumlah beli |
| price | DECIMAL(15,2) | NOT NULL | Harga beli saat itu |
| subtotal | DECIMAL(15,2) | NOT NULL | quantity × price |
| created_at | TIMESTAMP | ||
| updated_at | TIMESTAMP |
Tabel: transactions_out (Penjualan)
| Kolom | Tipe | Constraints | Keterangan |
|---|---|---|---|
| id | BIGINT UNSIGNED | PRIMARY KEY, AUTO_INCREMENT | |
| user_id | BIGINT UNSIGNED | FOREIGN KEY → users.id | Yang input |
| invoice_number | VARCHAR(50) | NOT NULL, UNIQUE | Auto-generate |
| transaction_date | DATE | NOT NULL | |
| subtotal | DECIMAL(15,2) | NOT NULL | Sum sebelum diskon |
| discount | DECIMAL(15,2) | DEFAULT 0 | |
| total_amount | DECIMAL(15,2) | NOT NULL | Final amount |
| notes | TEXT | NULLABLE | |
| created_at | TIMESTAMP | ||
| updated_at | TIMESTAMP | ||
| deleted_at | TIMESTAMP | NULLABLE |
Tabel: transaction_out_items
| Kolom | Tipe | Constraints | Keterangan |
|---|---|---|---|
| id | BIGINT UNSIGNED | PRIMARY KEY, AUTO_INCREMENT | |
| transaction_out_id | BIGINT UNSIGNED | FOREIGN KEY → transactions_out.id | |
| product_id | BIGINT UNSIGNED | FOREIGN KEY → products.id | |
| quantity | INTEGER | NOT NULL | Jumlah jual |
| price | DECIMAL(15,2) | NOT NULL | Harga jual saat itu |
| subtotal | DECIMAL(15,2) | NOT NULL | quantity × price |
| created_at | TIMESTAMP | ||
| updated_at | TIMESTAMP |
Tabel: activity_logs
| Kolom | Tipe | Constraints | Keterangan |
|---|---|---|---|
| id | BIGINT UNSIGNED | PRIMARY KEY, AUTO_INCREMENT | |
| user_id | BIGINT UNSIGNED | FOREIGN KEY → users.id, NULLABLE | NULL jika system |
| action | VARCHAR(50) | NOT NULL | created, updated, deleted |
| model_type | VARCHAR(255) | NOT NULL | App\Models\Product |
| model_id | BIGINT UNSIGNED | NOT NULL | |
| old_values | JSON | NULLABLE | Before state |
| new_values | JSON | NULLABLE | After state |
| ip_address | VARCHAR(45) | NULLABLE | |
| created_at | TIMESTAMP | ||
| updated_at | TIMESTAMP |
3. PENJELASAN RELASI
| Relasi | Tipe | Penjelasan |
|---|---|---|
| roles → users | One-to-Many | Satu role bisa dimiliki banyak users |
| categories → products | One-to-Many | Satu kategori bisa punya banyak produk |
| suppliers → transactions_in | One-to-Many | Satu supplier bisa punya banyak transaksi pembelian |
| users → transactions_in | One-to-Many | Satu user bisa input banyak transaksi |
| users → transactions_out | One-to-Many | Satu user bisa input banyak transaksi |
| transactions_in → transaction_in_items | One-to-Many | Satu transaksi punya banyak items |
| transactions_out → transaction_out_items | One-to-Many | Satu transaksi punya banyak items |
| products → transaction_in_items | One-to-Many | Satu produk bisa ada di banyak transaksi |
| products → transaction_out_items | One-to-Many | Satu produk bisa ada di banyak transaksi |
| users → activity_logs | One-to-Many | Satu user punya banyak activity logs |
4. RECOMMENDED INDEXES
-- Products: sering search by name, filter by category
CREATE INDEX idx_products_category ON products(category_id);
CREATE INDEX idx_products_name ON products(name);
CREATE INDEX idx_products_is_active ON products(is_active);
-- Transactions: sering filter by date range
CREATE INDEX idx_transactions_in_date ON transactions_in(transaction_date);
CREATE INDEX idx_transactions_out_date ON transactions_out(transaction_date);
-- Transaction Items: sering join dengan products
CREATE INDEX idx_transaction_in_items_product ON transaction_in_items(product_id);
CREATE INDEX idx_transaction_out_items_product ON transaction_out_items(product_id);
-- Activity Logs: sering filter by model
CREATE INDEX idx_activity_logs_model ON activity_logs(model_type, model_id);
CREATE INDEX idx_activity_logs_user ON activity_logs(user_id);
CREATE INDEX idx_activity_logs_created ON activity_logs(created_at);
Lanjutan: Generate Laravel Migrations
Setelah ERD di-review dan udah oke, saya minta Claude generate migrations.
Prompt:
Berdasarkan ERD di atas, buatkan Laravel 11 migration files untuk semua tabel.
Requirements:
- Urutkan file sesuai dependencies (tabel yang di-reference harus dibuat duluan)
- Gunakan Laravel conventions (timestamps, softDeletes helper)
- Include foreign key constraints dengan onDelete cascade/restrict yang appropriate
- Add indexes yang sudah direkomendasikan
- Setiap migration dalam code block terpisah dengan nama file
Mulai dari migration untuk roles sampai activity_logs.
Output Migration Files:
2024_01_01_000001_create_roles_table.php
<?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('roles', function (Blueprint $table) {
$table->id();
$table->string('name', 50)->unique();
$table->string('display_name', 100);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('roles');
}
};
2024_01_01_000002_add_role_to_users_table.php
<?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::table('users', function (Blueprint $table) {
$table->foreignId('role_id')->after('id')->constrained()->restrictOnDelete();
$table->string('phone', 20)->nullable()->after('email');
$table->boolean('is_active')->default(true)->after('phone');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropForeign(['role_id']);
$table->dropColumn(['role_id', 'phone', 'is_active']);
});
}
};
2024_01_01_000003_create_categories_table.php
<?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');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('categories');
}
};
2024_01_01_000004_create_products_table.php
<?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')->constrained()->restrictOnDelete();
$table->string('name');
$table->string('sku', 100)->unique();
$table->text('description')->nullable();
$table->string('image')->nullable();
$table->decimal('buy_price', 15, 2)->default(0);
$table->decimal('sell_price', 15, 2)->default(0);
$table->integer('stock')->default(0);
$table->integer('min_stock')->default(0);
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->softDeletes();
// Indexes
$table->index('category_id');
$table->index('name');
$table->index('is_active');
});
}
public function down(): void
{
Schema::dropIfExists('products');
}
};
2024_01_01_000005_create_suppliers_table.php
<?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('email')->nullable();
$table->string('phone', 20)->nullable();
$table->text('address')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('suppliers');
}
};
2024_01_01_000006_create_transactions_in_table.php
<?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('transactions_in', function (Blueprint $table) {
$table->id();
$table->foreignId('supplier_id')->constrained()->restrictOnDelete();
$table->foreignId('user_id')->constrained()->restrictOnDelete();
$table->string('invoice_number', 50)->unique();
$table->date('transaction_date');
$table->decimal('total_amount', 15, 2)->default(0);
$table->text('notes')->nullable();
$table->timestamps();
$table->softDeletes();
// Indexes
$table->index('transaction_date');
});
}
public function down(): void
{
Schema::dropIfExists('transactions_in');
}
};
2024_01_01_000007_create_transaction_in_items_table.php
<?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('transaction_in_items', function (Blueprint $table) {
$table->id();
$table->foreignId('transaction_in_id')->constrained('transactions_in')->cascadeOnDelete();
$table->foreignId('product_id')->constrained()->restrictOnDelete();
$table->integer('quantity');
$table->decimal('price', 15, 2);
$table->decimal('subtotal', 15, 2);
$table->timestamps();
// Indexes
$table->index('product_id');
});
}
public function down(): void
{
Schema::dropIfExists('transaction_in_items');
}
};
2024_01_01_000008_create_transactions_out_table.php
<?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('transactions_out', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->restrictOnDelete();
$table->string('invoice_number', 50)->unique();
$table->date('transaction_date');
$table->decimal('subtotal', 15, 2)->default(0);
$table->decimal('discount', 15, 2)->default(0);
$table->decimal('total_amount', 15, 2)->default(0);
$table->text('notes')->nullable();
$table->timestamps();
$table->softDeletes();
// Indexes
$table->index('transaction_date');
});
}
public function down(): void
{
Schema::dropIfExists('transactions_out');
}
};
2024_01_01_000009_create_transaction_out_items_table.php
<?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('transaction_out_items', function (Blueprint $table) {
$table->id();
$table->foreignId('transaction_out_id')->constrained('transactions_out')->cascadeOnDelete();
$table->foreignId('product_id')->constrained()->restrictOnDelete();
$table->integer('quantity');
$table->decimal('price', 15, 2);
$table->decimal('subtotal', 15, 2);
$table->timestamps();
// Indexes
$table->index('product_id');
});
}
public function down(): void
{
Schema::dropIfExists('transaction_out_items');
}
};
2024_01_01_000010_create_activity_logs_table.php
<?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('activity_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('action', 50);
$table->string('model_type');
$table->unsignedBigInteger('model_id');
$table->json('old_values')->nullable();
$table->json('new_values')->nullable();
$table->string('ip_address', 45)->nullable();
$table->timestamps();
// Indexes
$table->index(['model_type', 'model_id']);
$table->index('user_id');
$table->index('created_at');
});
}
public function down(): void
{
Schema::dropIfExists('activity_logs');
}
};
Review Checklist untuk ERD
Sebelum jalankan migration, saya selalu review:
Data Types
- [ ] Decimal precision cukup untuk currency? (15,2 biasanya cukup untuk Rupiah)
- [ ] String length appropriate? (VARCHAR 255 vs TEXT)
- [ ] Integer vs BigInteger untuk IDs?
Relationships
- [ ] Semua foreign keys defined?
- [ ] OnDelete behavior correct? (CASCADE vs RESTRICT vs SET NULL)
- [ ] Nullable foreign keys where appropriate?
Indexes
- [ ] Columns yang sering di-WHERE ada index?
- [ ] Columns yang sering di-ORDER BY ada index?
- [ ] Composite index untuk query patterns yang common?
Business Logic
- [ ] Semua fields yang dibutuhkan ada?
- [ ] Soft deletes di tempat yang tepat?
- [ ] Timestamps consistent?
Tips ERD dengan AI
Iterasi itu normal
Jarang sekali ERD pertama langsung perfect. Workflow saya:
- Generate draft pertama
- Review, find issues
- Minta AI revisi dengan feedback spesifik
- Repeat sampai solid
Kasih context business logic
Tambahan context:
- Harga beli produk bisa berubah setiap pembelian (dari supplier berbeda)
- Perlu track harga beli per batch untuk kalkulasi profit yang akurat
Apakah ERD perlu diubah untuk support ini?
AI akan suggest perubahan yang needed.
Minta AI explain trade-offs
Untuk activity_logs, apa trade-off antara:
1. Simpan di tabel sendiri (seperti sekarang)
2. Pakai package spatie/laravel-activitylog
3. Simpan di file log saja
Mana yang recommended untuk projek scale kecil-menengah?
Jangan takut challenge AI
Kalau ada yang nggak make sense, tanya:
Kenapa transaction_in_items pakai cascadeOnDelete tapi product_id pakai restrictOnDelete?
Apa rationale-nya?
AI akan explain, dan kalian bisa decide apakah setuju atau mau approach lain.
Checklist Sebelum Lanjut
- [ ] ERD sudah di-visualisasi dan di-review
- [ ] Semua relasi make sense untuk business logic
- [ ] Migrations sudah di-generate dan di-review
- [ ]
php artisan migrateberhasil tanpa error - [ ] Spot check dengan tinker: relationships work correctly
Kalau semua kecentang, database foundation sudah solid. Next step: define API contract sebelum mulai coding controllers.
Bagian 3: Membuat API Contract / Documentation
Database udah ready. Sebelum loncat ke coding, ada satu step yang sering di-skip freelancer: bikin API contract.
"Ah, nanti aja dokumentasinya setelah selesai."
Itu mindset yang bikin kerjaan double. Kenapa? Karena tanpa API contract yang clear di awal:
- Frontend developer (atau kalian sendiri) nggak tau struktur response
- Client nggak bisa review apakah API-nya sesuai kebutuhan
- Revisi di tengah jalan karena "oh ternyata butuh field ini juga"
- Inconsistency antara endpoints satu dengan lainnya
API contract itu bukan dokumentasi after-the-fact. Ini adalah blueprint sebelum coding.
Kenapa API Contract Penting untuk Freelancer
Parallel development Kalau projek ada frontend-nya (atau kalian kerjain sendiri tapi pisah phase), frontend bisa mulai duluan pakai mock data berdasarkan contract.
Client approval Contract bisa di-share ke client: "Ini API yang akan saya buat, sudah sesuai kebutuhan?" Lebih baik revisi di dokumen daripada revisi di code.
Self-documentation Ketika coding nanti, kalian tinggal follow contract. Nggak perlu mikir lagi struktur response-nya gimana.
Handover lebih smooth Kalau projek di-maintain orang lain, API docs sudah ready.
Gimana AI Membantu
AI bisa:
- Generate RESTful endpoints dari ERD/fitur
- Define request dan response format
- Suggest validation rules
- Generate OpenAPI/Swagger spec
- Ensure consistency across endpoints
Contoh: API Contract untuk Modul Product
Kita mulai dari satu modul dulu: Product. Setelah paham pattern-nya, modul lain tinggal repeat.
Prompt:
Berdasarkan tabel products di ERD inventory app:
products:
- id, category_id, name, sku, description, image
- buy_price, sell_price, stock, min_stock
- is_active, timestamps, soft_deletes
Relasi:
- belongsTo Category
- hasMany TransactionInItems, TransactionOutItems
Buatkan API contract lengkap untuk modul Product dengan:
1. List semua endpoints (method, URL, deskripsi singkat)
2. Detail setiap endpoint:
- Request headers yang diperlukan
- Request body/params (dengan tipe data)
- Response success (dengan contoh JSON)
- Response error (dengan contoh JSON)
- Validation rules untuk Laravel Form Request
Requirements:
- RESTful conventions
- Authentication pakai Laravel Sanctum (Bearer token)
- Pagination untuk list endpoint
- Support search by name dan filter by category
- Support sort by berbagai kolom
- Soft delete (archive, bukan permanent delete)
Format yang rapi dan mudah dibaca.
Output API Contract dari Claude
API CONTRACT: Product Module
Base URL: /api/v1
Authentication: Bearer Token (Laravel Sanctum)
Content-Type: application/json
ENDPOINTS OVERVIEW
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /products | List semua products (paginated) | Required |
| GET | /products/{id} | Detail satu product | Required |
| POST | /products | Create product baru | Required (Admin) |
| PUT | /products/{id} | Update product | Required (Admin) |
| DELETE | /products/{id} | Soft delete product | Required (Admin) |
| PATCH | /products/{id}/restore | Restore deleted product | Required (Admin) |
| POST | /products/{id}/image | Upload product image | Required (Admin) |
| DELETE | /products/{id}/image | Remove product image | Required (Admin) |
1. LIST PRODUCTS
GET /api/v1/products
Mengambil daftar products dengan pagination, search, filter, dan sort.
Request Headers:
Authorization: Bearer {token}
Accept: application/json
Query Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| page | integer | No | 1 | Halaman ke-n |
| per_page | integer | No | 15 | Jumlah per halaman (max 100) |
| search | string | No | - | Search by name atau SKU |
| category_id | integer | No | - | Filter by category |
| is_active | boolean | No | - | Filter by status (1/0) |
| low_stock | boolean | No | - | Filter stock <= min_stock |
| sort_by | string | No | created_at | Kolom untuk sorting |
| sort_order | string | No | desc | asc atau desc |
| with_trashed | boolean | No | false | Include soft deleted |
Example Request:
GET /api/v1/products?search=laptop&category_id=1&sort_by=name&sort_order=asc&page=1&per_page=10
Response Success (200):
{
"success": true,
"message": "Products retrieved successfully",
"data": {
"items": [
{
"id": 1,
"category_id": 1,
"category": {
"id": 1,
"name": "Electronics",
"slug": "electronics"
},
"name": "Laptop Asus ROG",
"sku": "LAP-ASUS-001",
"description": "Gaming laptop with RTX 4060",
"image": "<https://example.com/storage/products/laptop-asus.jpg>",
"image_url": "<https://example.com/storage/products/laptop-asus.jpg>",
"buy_price": 15000000,
"sell_price": 18000000,
"profit_margin": 3000000,
"profit_percentage": 20,
"stock": 25,
"min_stock": 5,
"is_low_stock": false,
"is_active": true,
"created_at": "2024-01-15T10:30:00.000000Z",
"updated_at": "2024-01-15T10:30:00.000000Z"
}
],
"pagination": {
"current_page": 1,
"last_page": 5,
"per_page": 10,
"total": 48,
"from": 1,
"to": 10,
"has_more_pages": true
}
}
}
Response Error - Unauthorized (401):
{
"success": false,
"message": "Unauthenticated",
"errors": null
}
2. GET PRODUCT DETAIL
GET /api/v1/products/{id}
Mengambil detail satu product.
Request Headers:
Authorization: Bearer {token}
Accept: application/json
URL Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Product ID |
Response Success (200):
{
"success": true,
"message": "Product retrieved successfully",
"data": {
"id": 1,
"category_id": 1,
"category": {
"id": 1,
"name": "Electronics",
"slug": "electronics"
},
"name": "Laptop Asus ROG",
"sku": "LAP-ASUS-001",
"description": "Gaming laptop with RTX 4060",
"image": "products/laptop-asus.jpg",
"image_url": "<https://example.com/storage/products/laptop-asus.jpg>",
"buy_price": 15000000,
"sell_price": 18000000,
"profit_margin": 3000000,
"profit_percentage": 20,
"stock": 25,
"min_stock": 5,
"is_low_stock": false,
"is_active": true,
"created_at": "2024-01-15T10:30:00.000000Z",
"updated_at": "2024-01-15T10:30:00.000000Z",
"deleted_at": null,
"stock_history": {
"total_in": 50,
"total_out": 25,
"last_restock": "2024-01-10T08:00:00.000000Z",
"last_sale": "2024-01-14T15:30:00.000000Z"
}
}
}
Response Error - Not Found (404):
{
"success": false,
"message": "Product not found",
"errors": null
}
3. CREATE PRODUCT
POST /api/v1/products
Membuat product baru. Hanya Admin yang bisa akses.
Request Headers:
Authorization: Bearer {token}
Accept: application/json
Content-Type: application/json
Request Body:
{
"category_id": 1,
"name": "Laptop Asus ROG",
"sku": "LAP-ASUS-001",
"description": "Gaming laptop with RTX 4060",
"buy_price": 15000000,
"sell_price": 18000000,
"stock": 25,
"min_stock": 5,
"is_active": true
}
Validation Rules (Laravel Form Request):
public function rules(): array
{
return [
'category_id' => ['required', 'integer', 'exists:categories,id'],
'name' => ['required', 'string', 'max:255'],
'sku' => ['required', 'string', 'max:100', 'unique:products,sku'],
'description' => ['nullable', 'string', 'max:5000'],
'buy_price' => ['required', 'numeric', 'min:0', 'max:999999999999.99'],
'sell_price' => ['required', 'numeric', 'min:0', 'max:999999999999.99', 'gte:buy_price'],
'stock' => ['required', 'integer', 'min:0'],
'min_stock' => ['required', 'integer', 'min:0'],
'is_active' => ['boolean'],
];
}
public function messages(): array
{
return [
'category_id.exists' => 'Kategori tidak ditemukan',
'sku.unique' => 'SKU sudah digunakan produk lain',
'sell_price.gte' => 'Harga jual harus lebih besar atau sama dengan harga beli',
];
}
Response Success (201):
{
"success": true,
"message": "Product created successfully",
"data": {
"id": 2,
"category_id": 1,
"name": "Laptop Asus ROG",
"sku": "LAP-ASUS-001",
"description": "Gaming laptop with RTX 4060",
"image": null,
"image_url": null,
"buy_price": 15000000,
"sell_price": 18000000,
"stock": 25,
"min_stock": 5,
"is_active": true,
"created_at": "2024-01-15T10:30:00.000000Z",
"updated_at": "2024-01-15T10:30:00.000000Z"
}
}
Response Error - Validation (422):
{
"success": false,
"message": "Validation error",
"errors": {
"sku": ["SKU sudah digunakan produk lain"],
"sell_price": ["Harga jual harus lebih besar atau sama dengan harga beli"]
}
}
Response Error - Forbidden (403):
{
"success": false,
"message": "You do not have permission to perform this action",
"errors": null
}
4. UPDATE PRODUCT
PUT /api/v1/products/{id}
Update product existing. Hanya Admin yang bisa akses.
Request Headers:
Authorization: Bearer {token}
Accept: application/json
Content-Type: application/json
URL Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | integer | Yes | Product ID |
Request Body:
{
"category_id": 1,
"name": "Laptop Asus ROG Updated",
"sku": "LAP-ASUS-001",
"description": "Gaming laptop with RTX 4060 - Updated description",
"buy_price": 15000000,
"sell_price": 19000000,
"stock": 30,
"min_stock": 5,
"is_active": true
}
Validation Rules:
public function rules(): array
{
$productId = $this->route('product');
return [
'category_id' => ['required', 'integer', 'exists:categories,id'],
'name' => ['required', 'string', 'max:255'],
'sku' => ['required', 'string', 'max:100', "unique:products,sku,{$productId}"],
'description' => ['nullable', 'string', 'max:5000'],
'buy_price' => ['required', 'numeric', 'min:0', 'max:999999999999.99'],
'sell_price' => ['required', 'numeric', 'min:0', 'max:999999999999.99', 'gte:buy_price'],
'stock' => ['required', 'integer', 'min:0'],
'min_stock' => ['required', 'integer', 'min:0'],
'is_active' => ['boolean'],
];
}
Response Success (200):
{
"success": true,
"message": "Product updated successfully",
"data": {
"id": 1,
"category_id": 1,
"name": "Laptop Asus ROG Updated",
"sku": "LAP-ASUS-001",
"description": "Gaming laptop with RTX 4060 - Updated description",
"image": "products/laptop-asus.jpg",
"image_url": "<https://example.com/storage/products/laptop-asus.jpg>",
"buy_price": 15000000,
"sell_price": 19000000,
"stock": 30,
"min_stock": 5,
"is_active": true,
"created_at": "2024-01-15T10:30:00.000000Z",
"updated_at": "2024-01-15T14:00:00.000000Z"
}
}
5. DELETE PRODUCT (Soft Delete)
DELETE /api/v1/products/{id}
Soft delete product (archive). Hanya Admin yang bisa akses.
Request Headers:
Authorization: Bearer {token}
Accept: application/json
Response Success (200):
{
"success": true,
"message": "Product deleted successfully",
"data": null
}
Response Error - Has Transactions (422):
{
"success": false,
"message": "Cannot delete product with existing transactions. Consider deactivating instead.",
"errors": {
"product": ["Product has 15 transaction records"]
}
}
6. RESTORE PRODUCT
PATCH /api/v1/products/{id}/restore
Restore soft-deleted product. Hanya Admin yang bisa akses.
Response Success (200):
{
"success": true,
"message": "Product restored successfully",
"data": {
"id": 1,
"name": "Laptop Asus ROG",
"deleted_at": null
}
}
7. UPLOAD PRODUCT IMAGE
POST /api/v1/products/{id}/image
Upload atau replace product image.
Request Headers:
Authorization: Bearer {token}
Accept: application/json
Content-Type: multipart/form-data
Request Body (Form Data):
| Field | Type | Required | Description |
|---|---|---|---|
| image | file | Yes | Image file (jpg, png, webp) |
Validation Rules:
public function rules(): array
{
return [
'image' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'], // max 2MB
];
}
Response Success (200):
{
"success": true,
"message": "Image uploaded successfully",
"data": {
"image": "products/laptop-asus-abc123.jpg",
"image_url": "<https://example.com/storage/products/laptop-asus-abc123.jpg>"
}
}
8. DELETE PRODUCT IMAGE
DELETE /api/v1/products/{id}/image
Remove product image.
Response Success (200):
{
"success": true,
"message": "Image removed successfully",
"data": null
}
STANDARD RESPONSE FORMAT
Success Response Structure:
{
"success": true,
"message": "Human readable message",
"data": { } // atau array, atau null
}
Error Response Structure:
{
"success": false,
"message": "Human readable error message",
"errors": {
"field_name": ["Error message 1", "Error message 2"]
}
}
HTTP Status Codes Used:
| Code | Meaning |
|---|---|
| 200 | OK - Request successful |
| 201 | Created - Resource created |
| 401 | Unauthorized - Token invalid/missing |
| 403 | Forbidden - No permission |
| 404 | Not Found - Resource not found |
| 422 | Unprocessable - Validation error |
| 500 | Server Error - Something went wrong |
Generate API Contract untuk Modul Lain
Setelah punya template dari Product, generate untuk modul lain jadi lebih gampang.
Prompt untuk modul Transaction:
Menggunakan format API contract yang sama dengan modul Product di atas,
buatkan API contract untuk modul Transaction Out (Penjualan) dengan:
Tabel terkait:
- transactions_out: id, user_id, invoice_number, transaction_date, subtotal, discount, total_amount, notes
- transaction_out_items: id, transaction_out_id, product_id, quantity, price, subtotal
Endpoints yang dibutuhkan:
1. List transactions (dengan filter date range, pagination)
2. Get transaction detail (include items dan products)
3. Create transaction (dengan multiple items, auto-update stock, auto-generate invoice number)
4. Cancel/void transaction (soft delete, restore stock)
5. Get daily/monthly summary
Special requirements:
- Validasi stock availability sebelum create
- Auto-calculate subtotal dan total
- Invoice number format: INV-OUT-YYYYMMDD-XXXX
Gunakan response format yang konsisten dengan modul Product.
Tips API Contract dengan AI
Konsistensi adalah kunci
Pastikan format response sama di semua endpoints. AI kadang inconsistent, jadi review dan standardize.
Minta AI generate Form Request class
Dari validation rules di API contract, buatkan Laravel Form Request class
lengkap untuk StoreProductRequest dan UpdateProductRequest.
Include:
- authorize() method dengan role check
- rules() method
- messages() untuk custom error messages Indonesia
- prepareForValidation() kalau perlu data transformation
Generate API Resource juga
Buatkan Laravel API Resource class untuk Product yang menghasilkan
response format sesuai API contract di atas.
Include:
- ProductResource untuk single product
- ProductCollection untuk list dengan pagination
- Conditional attributes (when loaded relationships)
- Computed attributes (profit_margin, profit_percentage, is_low_stock)
Export ke OpenAPI/Swagger
Convert API contract modul Product di atas ke format OpenAPI 3.0 (YAML).
Pastikan include:
- All endpoints dengan parameters
- Request body schemas
- Response schemas untuk semua status codes
- Authentication scheme (Bearer token)
- Example values
Output yang bisa langsung di-import ke Swagger UI atau Postman.
Workflow Lengkap: Dari Contract ke Implementation
Setelah API contract approved, ini workflow saya:
1. API Contract (document)
↓
2. Generate Form Request classes (AI assist)
↓
3. Generate API Resource classes (AI assist)
↓
4. Generate Controller skeleton (AI assist)
↓
5. Implement business logic (manual + AI assist)
↓
6. Write tests (AI assist)
↓
7. Test dengan Postman/Insomnia
↓
8. Update contract kalau ada perubahan
Dengan contract di awal, step 2-4 bisa heavily assisted by AI karena spec-nya udah jelas.
Checklist API Contract
Sebelum lanjut ke development:
- [ ] Semua endpoints terdefinisi dengan jelas
- [ ] Request format (headers, body, params) documented
- [ ] Response format untuk success DAN error documented
- [ ] Validation rules ready untuk implementation
- [ ] Consistent format across all endpoints
- [ ] Client/stakeholder sudah review dan approve (kalau applicable)
Contract yang solid = development yang lebih smooth. Next, kita bahas User Flow sebelum actual coding.
Bagian 4: Design User Flow dan Wireframe Description
Oke, database ready, API contract ready. Sekarang sebelum coding, ada satu step lagi yang sering di-skip: mikirin user flow.
"Kan udah ada API contract, tinggal coding aja."
Bisa sih. Tapi nanti di tengah jalan:
- "Eh, setelah user submit form, redirect ke mana ya?"
- "Kalau stok habis pas checkout, flow-nya gimana?"
- "Tombol cancel ini behavior-nya apa?"
Pertanyaan-pertanyaan ini kalau dijawab sambil coding = inconsistent UX dan banyak revisi.
User flow bukan cuma untuk designer. Untuk freelancer yang handle full-stack sendiri, ini equally penting.
Kenapa User Flow Penting
Prevent "coding tanpa arah"
Dengan user flow, kalian tau exact sequence: user klik apa → sistem respond apa → user lihat apa. Coding jadi lebih terarah.
Catch edge cases lebih awal
"Kalau user input quantity lebih dari stok gimana?" — pertanyaan kayak gini muncul natural waktu bikin flow.
Communication tool dengan client
Flow lebih gampang dipahami client daripada technical spec. Bisa dapat approval sebelum coding.
Basis untuk testing later
User flow = test scenarios. Setiap path di flow = test case yang harus pass.
Gimana AI Membantu
AI bisa:
- Generate step-by-step flow dari fitur description
- Describe wireframe tanpa harus actual design
- Identify decision points dan edge cases
- Generate flowchart dalam Mermaid format
- Suggest UX improvements
Contoh: User Flow untuk "Catat Penjualan"
Ini salah satu fitur core di inventory app. Let's design flow-nya dengan bantuan AI.
Prompt:
Untuk aplikasi inventory, buatkan user flow lengkap untuk proses:
"Staff mencatat transaksi penjualan"
Context:
- User sudah login sebagai Staff
- Transaksi bisa punya multiple items (products)
- Harus cek ketersediaan stok
- Bisa kasih diskon per transaksi
- Generate invoice number otomatis
- Print/download struk setelah selesai
Tolong describe:
1. Step-by-step flow dari mulai sampai selesai
2. Decision points (kalau stok habis, kalau input invalid, dll)
3. Wireframe description untuk setiap screen utama (describe layout dan elements, bukan actual design)
4. Edge cases yang perlu di-handle
5. Success dan error states di setiap step
6. Alternative flows (kalau user cancel di tengah jalan, dll)
Format yang mudah dibaca dan bisa di-share ke client non-technical.
Output User Flow dari Claude
USER FLOW: Catat Transaksi Penjualan
Actor: Staff (sudah login)
Goal: Mencatat penjualan dan mencetak struk
Precondition: User memiliki role Staff atau Admin
MAIN FLOW (Happy Path)
┌─────────────────────────────────────────────────────────────────┐
│ MAIN FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [1] Dashboard │
│ │ │
│ ▼ │
│ [2] Klik "Transaksi Baru" / "Penjualan Baru" │
│ │ │
│ ▼ │
│ [3] Halaman Form Transaksi │
│ │ │
│ ├──► [4] Cari & Tambah Produk ◄────┐ │
│ │ │ │ │
│ │ ▼ │ │
│ │ [5] Produk ditambah ke cart │ │
│ │ │ │ │
│ │ └── (ulangi jika perlu) ──┘ │
│ │ │
│ ▼ │
│ [6] Review Cart & Input Diskon (optional) │
│ │ │
│ ▼ │
│ [7] Klik "Proses Transaksi" │
│ │ │
│ ▼ │
│ [8] Konfirmasi Dialog │
│ │ │
│ ▼ │
│ [9] Transaksi Berhasil - Invoice Created │
│ │ │
│ ▼ │
│ [10] Print/Download Struk atau Kembali ke Dashboard │
│ │
└─────────────────────────────────────────────────────────────────┘
DETAILED STEPS
Step 1-2: Navigasi ke Form Transaksi
| Step | User Action | System Response |
|---|---|---|
| 1 | User di Dashboard | Menampilkan summary dan quick actions |
| 2 | Klik tombol "Penjualan Baru" | Navigate ke halaman form transaksi, generate draft invoice number |
Step 3: Halaman Form Transaksi (Initial State)
Wireframe Description:
┌─────────────────────────────────────────────────────────────┐
│ ← Kembali TRANSAKSI PENJUALAN [Batalkan] │
├─────────────────────────────────────────────────────────────┤
│ │
│ Invoice: INV-OUT-20240115-0001 (auto) Tanggal: [Today] │
│ │
├─────────────────────────────────────────────────────────────┤
│ CARI PRODUK │
│ ┌─────────────────────────────────────────────┐ │
│ │ 🔍 Ketik nama produk atau scan barcode... │ [Cari] │
│ └─────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────┤
│ KERANJANG (0 item) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Belum ada produk ditambahkan │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
├─────────────────────────────────────────────────────────────┤
│ RINGKASAN │
│ Subtotal: Rp 0 │
│ Diskon: [___________] -Rp 0 │
│ ───────────────────────────────────────────────────── │
│ TOTAL: Rp 0 │
│ │
│ [ Proses Transaksi ] (disabled)│
│ │
└─────────────────────────────────────────────────────────────┘
Elements:
- Header dengan invoice number (auto-generated, read-only)
- Date picker (default today, bisa diubah)
- Search box untuk cari produk (by name atau SKU)
- Cart area (empty state saat awal)
- Summary section dengan subtotal, diskon input, total
- Submit button (disabled sampai ada item di cart)
- Cancel button untuk batalkan transaksi
Step 4: Cari & Pilih Produk
| User Action | System Response |
|---|---|
| Ketik nama produk di search box | Tampilkan dropdown hasil search (realtime, debounced 300ms) |
| Klik salah satu produk dari hasil | Tampilkan modal/popup untuk input quantity |
Search Results Dropdown:
┌─────────────────────────────────────────────────────────────┐
│ 🔍 laptop │
├─────────────────────────────────────────────────────────────┤
│ 📦 Laptop Asus ROG │
│ SKU: LAP-ASUS-001 | Stok: 25 | Rp 18.000.000 │
├─────────────────────────────────────────────────────────────┤
│ 📦 Laptop Lenovo ThinkPad │
│ SKU: LAP-LEN-001 | Stok: 12 | Rp 15.000.000 │
├─────────────────────────────────────────────────────────────┤
│ 📦 Laptop HP Pavilion │
│ SKU: LAP-HP-001 | Stok: 0 | Rp 12.000.000 [Habis] │
└─────────────────────────────────────────────────────────────┘
Notes:
- Produk dengan stok 0 tetap muncul tapi di-gray out dan ada label "Habis"
- Tampilkan stok realtime untuk hindari overselling
- Klik produk stok 0 → show toast "Stok produk habis"
Step 5: Input Quantity
Quantity Input Modal:
┌─────────────────────────────────────────────────────────────┐
│ TAMBAH KE KERANJANG │
├─────────────────────────────────────────────────────────────┤
│ │
│ Laptop Asus ROG │
│ SKU: LAP-ASUS-001 │
│ Harga: Rp 18.000.000 │
│ Stok tersedia: 25 │
│ │
│ Jumlah: [ - ] [ 1 ] [ + ] │
│ │
│ Subtotal: Rp 18.000.000 │
│ │
│ [Batal] [Tambahkan] │
│ │
└─────────────────────────────────────────────────────────────┘
Validation:
- Quantity minimal 1
- Quantity maksimal = stok tersedia
- Tombol + disabled kalau sudah = stok
- Show error kalau manual input melebihi stok
Decision Point - Stok Tidak Cukup:
IF quantity > available_stock:
→ Show error message: "Stok tidak mencukupi. Tersedia: {stock}"
→ Keep modal open
→ Highlight input field red
ELSE:
→ Add to cart
→ Close modal
→ Update cart display
→ Show success toast: "Produk ditambahkan"
Step 5 Result: Cart Updated
Wireframe dengan Items di Cart:
┌─────────────────────────────────────────────────────────────┐
│ KERANJANG (2 items) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. Laptop Asus ROG │ │
│ │ SKU: LAP-ASUS-001 │ │
│ │ Harga: Rp 18.000.000 │ │
│ │ Qty: [ - ] [ 2 ] [ + ] Subtotal: Rp 36.000.000│ │
│ │ [🗑️] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 2. Mouse Logitech │ │
│ │ SKU: ACC-MOU-001 │ │
│ │ Harga: Rp 500.000 │ │
│ │ Qty: [ - ] [ 1 ] [ + ] Subtotal: Rp 500.000│ │
│ │ [🗑️] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Cart Item Interactions:
- Quantity bisa diubah langsung dari cart (+ / - buttons)
- Delete icon untuk hapus item
- Subtotal auto-update saat quantity berubah
- Total di summary auto-update
Step 6: Review & Input Diskon
| User Action | System Response |
|---|---|
| Review items di cart | Display semua items dengan subtotal |
| (Optional) Input nominal diskon | Recalculate total |
| (Optional) Ubah quantity | Update subtotal dan total |
| (Optional) Hapus item | Remove dari cart, update total |
Diskon Validation:
- Diskon tidak boleh lebih besar dari subtotal
- Diskon minimal 0
- Show error jika input invalid
Step 7-8: Proses Transaksi
| User Action | System Response |
|---|---|
| Klik "Proses Transaksi" | Tampilkan konfirmasi dialog |
| Klik "Ya, Proses" di dialog | Submit ke API, show loading |
Confirmation Dialog:
┌─────────────────────────────────────────────────────────────┐
│ KONFIRMASI TRANSAKSI │
├─────────────────────────────────────────────────────────────┤
│ │
│ Anda akan memproses transaksi: │
│ │
│ Invoice: INV-OUT-20240115-0001 │
│ Total Items: 2 produk (3 qty) │
│ Total Bayar: Rp 36.500.000 │
│ │
│ ⚠️ Transaksi yang sudah diproses tidak bisa diubah. │
│ Stok produk akan berkurang otomatis. │
│ │
│ [Batal] [Ya, Proses] │
│ │
└─────────────────────────────────────────────────────────────┘
Step 9-10: Transaksi Berhasil
Success State:
┌─────────────────────────────────────────────────────────────┐
│ │
│ ✅ │
│ │
│ TRANSAKSI BERHASIL │
│ │
│ Invoice: INV-OUT-20240115-0001 │
│ Total: Rp 36.500.000 │
│ │
│ [🖨️ Print Struk] [📥 Download PDF] │
│ │
│ [Transaksi Baru] [Kembali ke Dashboard] │
│ │
└─────────────────────────────────────────────────────────────┘
System Actions (Background):
- Create transaction record
- Create transaction items
- Reduce stock untuk setiap product
- Log activity
EDGE CASES & ERROR HANDLING
Edge Case 1: Stok Berubah Saat di Cart
Scenario: User A menambahkan produk X (stok 5) ke cart,
User B (di komputer lain) beli produk X 3 pcs,
User A mau checkout dengan qty 5
Solution:
- Saat submit, re-validate stock dari database
- Jika stok tidak cukup:
→ Show error: "Stok {product} berubah. Tersedia: {current_stock}"
→ Highlight item yang bermasalah
→ User harus adjust quantity atau hapus item
→ Jangan auto-adjust (biar user aware)
Edge Case 2: Produk Di-nonaktifkan Saat di Cart
Scenario: Admin menonaktifkan produk saat produk ada di cart user lain
Solution:
- Saat submit, check is_active
- Jika inactive:
→ Show error: "Produk {name} sudah tidak tersedia"
→ Remove dari cart otomatis
→ User harus review dan submit ulang
Edge Case 3: Session Timeout
Scenario: User isi cart, ditinggal lama, session expired
Solution:
- Cart data simpan di localStorage (frontend)
- Saat session expired dan user login lagi:
→ Restore cart dari localStorage
→ Re-validate semua items (stock, price, active status)
→ Notify jika ada perubahan
Edge Case 4: Duplicate Submit
Scenario: User klik submit 2x cepat (double click)
Solution:
- Disable button saat processing
- Show loading indicator
- Backend: check for recent duplicate invoice (within 5 seconds)
- Idempotency key (optional, untuk extra safety)
Edge Case 5: Network Error Saat Submit
Scenario: Submit tapi network error / timeout
Solution:
- Show error: "Gagal memproses. Periksa koneksi dan coba lagi."
- Keep cart data intact
- Provide retry button
- JANGAN auto-retry (bisa duplicate transaction)
ALTERNATIVE FLOWS
Alt Flow 1: User Batalkan di Tengah Jalan
User klik "Batalkan" atau "← Kembali" saat cart ada isi
│
▼
Show confirmation: "Cart masih ada isi. Yakin mau keluar?"
│
├── [Tetap di sini] → Close dialog, stay on page
│
└── [Ya, keluar] → Clear cart, navigate back
Alt Flow 2: Edit Quantity di Cart Melebihi Stok
User ubah quantity menjadi lebih dari stok
│
▼
System auto-set ke max available stock
│
▼
Show toast: "Quantity disesuaikan dengan stok tersedia ({stock})"
Alt Flow 3: Scan Barcode (Future Enhancement)
User scan barcode produk
│
▼
System lookup by SKU/barcode
│
├── Found → Auto-open quantity modal (same as search flow)
│
└── Not found → Show error: "Produk tidak ditemukan"
MERMAID FLOWCHART
flowchart TD
A[Dashboard] --> B[Klik Penjualan Baru]
B --> C[Form Transaksi]
C --> D[Cari Produk]
D --> E{Produk Ditemukan?}
E -->|Ya| F{Stok Tersedia?}
E -->|Tidak| D
F -->|Ya| G[Input Quantity]
F -->|Tidak| H[Show 'Stok Habis']
H --> D
G --> I{Qty <= Stok?}
I -->|Ya| J[Add to Cart]
I -->|Tidak| K[Show Error]
K --> G
J --> L{Tambah Lagi?}
L -->|Ya| D
L -->|Tidak| M[Review Cart]
M --> N[Input Diskon Optional]
N --> O[Klik Proses]
O --> P[Konfirmasi Dialog]
P --> Q{Konfirmasi?}
Q -->|Ya| R[Submit ke API]
Q -->|Tidak| M
R --> S{API Success?}
S -->|Ya| T[Reduce Stock]
T --> U[Show Success]
U --> V{Print Struk?}
V -->|Ya| W[Print/Download]
V -->|Tidak| X[Done]
W --> X
S -->|Tidak| Y[Show Error]
Y --> M
Prompt Lanjutan: Generate Wireframe untuk Screen Lain
Menggunakan format wireframe description yang sama, buatkan untuk screen:
"Halaman List Transaksi Penjualan"
Requirements:
- Table view dengan pagination
- Filter by date range
- Search by invoice number
- Sort by date, total
- Quick actions: view detail, print, void
- Summary cards di atas (total hari ini, total bulan ini)
Describe layout, elements, dan interactions.
Tips User Flow dengan AI
Describe context dengan lengkap
AI nggak tau app kalian kecuali dijelasin. Sebutin role user, existing features, constraints.
Minta edge cases explicitly
AI kadang cuma kasih happy path. Explicitly minta: "Apa saja edge cases yang perlu dihandle?"
Iterate based on feedback
Di flow Step 5, saya mau behaviour yang berbeda:
- Jika user sudah pernah tambah produk yang sama, langsung increment quantity,
jangan buka modal lagi.
- Update flow dan wireframe description sesuai perubahan ini.
Convert ke format lain
Convert user flow di atas ke format:
1. User Story dengan acceptance criteria (Gherkin format)
2. Test cases untuk QA
Checklist User Flow
Sebelum mulai coding:
- [ ] Main flow (happy path) terdokumentasi dengan jelas
- [ ] Decision points dan branching logic defined
- [ ] Edge cases identified dan solution planned
- [ ] Error states dan messages defined
- [ ] Wireframe description cukup detail untuk implement
- [ ] Alternative flows documented
- [ ] Flow sudah di-review (self-review atau dengan client)
Dengan user flow yang solid, coding jadi lebih terarah. Kalian tau exactly apa yang harus di-build. Next, kita masuk ke actual coding — scaffolding dengan bantuan AI.
Bagian 5: Scaffolding Code — Model, Migration, Controller, Routes
Akhirnya, saatnya coding!
Tapi sebelum excited nulis business logic yang kompleks, ada banyak boilerplate yang harus dibuat dulu:
- Model dengan relationships
- Migrations (udah dari Bagian 2)
- Controllers dengan method stubs
- Form Requests untuk validation
- API Resources untuk response formatting
- Routes definition
Kalau dihitung, untuk satu modul CRUD sederhana aja, kalian butuh minimal 5-7 files. Kalikan dengan jumlah modul — itu banyak waktu untuk code yang essentially repetitive.
Di sinilah AI jadi productivity booster yang signifikan.
Workflow Scaffolding dengan AI
Workflow saya:
1. Define requirements per modul
↓
2. Generate Model (AI)
↓
3. Generate Form Requests (AI)
↓
4. Generate API Resource (AI)
↓
5. Generate Controller (AI)
↓
6. Define Routes (AI atau manual)
↓
7. Review semua files
↓
8. Adjust dan refine
↓
9. Test basic functionality
Dengan AI, step 2-6 bisa selesai dalam hitungan menit, bukan jam.
Contoh: Scaffolding Modul Product
Let's generate semua files untuk modul Product.
1. Generate Model
Prompt:
Buatkan Laravel 11 Model untuk Product dengan spesifikasi:
Tabel: products
Columns: id, category_id, name, sku, description, image, buy_price, sell_price, stock, min_stock, is_active, timestamps, soft_deletes
Relationships:
- belongsTo Category
- hasMany TransactionInItem
- hasMany TransactionOutItem
Include:
- $fillable
- $casts dengan tipe yang appropriate
- Relationships methods
- Useful scopes: active(), lowStock(), byCategory($categoryId), search($term)
- Accessors: image_url (full URL ke storage), profit_margin (sell - buy), profit_percentage
- Boot method untuk auto-generate SKU jika kosong
Gunakan PHP 8.2+ syntax dengan type hints dan return types.
Output:
<?php
namespace App\\Models;
use Illuminate\\Database\\Eloquent\\Factories\\HasFactory;
use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;
use Illuminate\\Database\\Eloquent\\Relations\\HasMany;
use Illuminate\\Database\\Eloquent\\SoftDeletes;
use Illuminate\\Database\\Eloquent\\Builder;
use Illuminate\\Database\\Eloquent\\Casts\\Attribute;
use Illuminate\\Support\\Str;
use Illuminate\\Support\\Facades\\Storage;
class Product extends Model
{
use HasFactory, SoftDeletes;
/**
* The attributes that are mass assignable.
*/
protected $fillable = [
'category_id',
'name',
'sku',
'description',
'image',
'buy_price',
'sell_price',
'stock',
'min_stock',
'is_active',
];
/**
* The attributes that should be cast.
*/
protected $casts = [
'category_id' => 'integer',
'buy_price' => 'decimal:2',
'sell_price' => 'decimal:2',
'stock' => 'integer',
'min_stock' => 'integer',
'is_active' => 'boolean',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime',
];
/**
* The accessors to append to the model's array form.
*/
protected $appends = [
'image_url',
'profit_margin',
'profit_percentage',
'is_low_stock',
];
/**
* Boot the model.
*/
protected static function boot(): void
{
parent::boot();
static::creating(function (Product $product) {
if (empty($product->sku)) {
$product->sku = self::generateSku();
}
});
}
/**
* Generate unique SKU.
*/
public static function generateSku(): string
{
do {
$sku = 'PRD-' . strtoupper(Str::random(8));
} while (self::where('sku', $sku)->exists());
return $sku;
}
// ==================== RELATIONSHIPS ====================
/**
* Get the category that owns the product.
*/
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
/**
* Get the transaction in items for the product.
*/
public function transactionInItems(): HasMany
{
return $this->hasMany(TransactionInItem::class);
}
/**
* Get the transaction out items for the product.
*/
public function transactionOutItems(): HasMany
{
return $this->hasMany(TransactionOutItem::class);
}
// ==================== ACCESSORS ====================
/**
* Get the full URL to the product image.
*/
protected function imageUrl(): Attribute
{
return Attribute::make(
get: fn () => $this->image
? Storage::disk('public')->url($this->image)
: null
);
}
/**
* Get the profit margin (sell_price - buy_price).
*/
protected function profitMargin(): Attribute
{
return Attribute::make(
get: fn () => $this->sell_price - $this->buy_price
);
}
/**
* Get the profit percentage.
*/
protected function profitPercentage(): Attribute
{
return Attribute::make(
get: fn () => $this->buy_price > 0
? round((($this->sell_price - $this->buy_price) / $this->buy_price) * 100, 2)
: 0
);
}
/**
* Check if product is low on stock.
*/
protected function isLowStock(): Attribute
{
return Attribute::make(
get: fn () => $this->stock <= $this->min_stock
);
}
// ==================== SCOPES ====================
/**
* Scope to only include active products.
*/
public function scopeActive(Builder $query): Builder
{
return $query->where('is_active', true);
}
/**
* Scope to only include inactive products.
*/
public function scopeInactive(Builder $query): Builder
{
return $query->where('is_active', false);
}
/**
* Scope to only include low stock products.
*/
public function scopeLowStock(Builder $query): Builder
{
return $query->whereColumn('stock', '<=', 'min_stock');
}
/**
* Scope to filter by category.
*/
public function scopeByCategory(Builder $query, int $categoryId): Builder
{
return $query->where('category_id', $categoryId);
}
/**
* Scope to search by name or SKU.
*/
public function scopeSearch(Builder $query, ?string $term): Builder
{
if (empty($term)) {
return $query;
}
return $query->where(function (Builder $q) use ($term) {
$q->where('name', 'like', "%{$term}%")
->orWhere('sku', 'like', "%{$term}%");
});
}
/**
* Scope to sort by given column and direction.
*/
public function scopeSortBy(Builder $query, string $column = 'created_at', string $direction = 'desc'): Builder
{
$allowedColumns = ['name', 'sku', 'buy_price', 'sell_price', 'stock', 'created_at', 'updated_at'];
if (!in_array($column, $allowedColumns)) {
$column = 'created_at';
}
$direction = strtolower($direction) === 'asc' ? 'asc' : 'desc';
return $query->orderBy($column, $direction);
}
// ==================== HELPER METHODS ====================
/**
* Check if product has enough stock.
*/
public function hasStock(int $quantity = 1): bool
{
return $this->stock >= $quantity;
}
/**
* Reduce stock.
*/
public function reduceStock(int $quantity): bool
{
if (!$this->hasStock($quantity)) {
return false;
}
$this->decrement('stock', $quantity);
return true;
}
/**
* Add stock.
*/
public function addStock(int $quantity): void
{
$this->increment('stock', $quantity);
}
/**
* Check if product can be deleted.
*/
public function canBeDeleted(): bool
{
return $this->transactionInItems()->count() === 0
&& $this->transactionOutItems()->count() === 0;
}
}
2. Generate Form Requests
Prompt:
Buatkan Laravel Form Request classes untuk Product:
1. StoreProductRequest - untuk create product baru
2. UpdateProductRequest - untuk update product
Berdasarkan API contract:
- category_id: required, exists di categories
- name: required, max 255
- sku: required, max 100, unique (kecuali saat update)
- description: optional, max 5000
- buy_price: required, numeric, min 0
- sell_price: required, numeric, min 0, harus >= buy_price
- stock: required, integer, min 0
- min_stock: required, integer, min 0
- is_active: boolean
Include:
- authorize() method (hanya Admin yang bisa)
- rules() dengan proper validation
- messages() dengan pesan error Bahasa Indonesia
- prepareForValidation() untuk sanitize input jika perlu
Gunakan PHP 8.2+ syntax.
Output:
StoreProductRequest.php:
<?php
namespace App\\Http\\Requests\\Product;
use Illuminate\\Foundation\\Http\\FormRequest;
use Illuminate\\Validation\\Rule;
class StoreProductRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->user()->isAdmin();
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'name' => $this->name ? trim($this->name) : null,
'sku' => $this->sku ? strtoupper(trim($this->sku)) : null,
'description' => $this->description ? trim($this->description) : null,
'is_active' => $this->boolean('is_active', true),
]);
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'category_id' => [
'required',
'integer',
Rule::exists('categories', 'id'),
],
'name' => [
'required',
'string',
'max:255',
],
'sku' => [
'required',
'string',
'max:100',
Rule::unique('products', 'sku'),
],
'description' => [
'nullable',
'string',
'max:5000',
],
'buy_price' => [
'required',
'numeric',
'min:0',
'max:999999999999.99',
],
'sell_price' => [
'required',
'numeric',
'min:0',
'max:999999999999.99',
'gte:buy_price',
],
'stock' => [
'required',
'integer',
'min:0',
],
'min_stock' => [
'required',
'integer',
'min:0',
],
'is_active' => [
'boolean',
],
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'category_id.required' => 'Kategori wajib dipilih.',
'category_id.exists' => 'Kategori yang dipilih tidak valid.',
'name.required' => 'Nama produk wajib diisi.',
'name.max' => 'Nama produk maksimal 255 karakter.',
'sku.required' => 'SKU wajib diisi.',
'sku.max' => 'SKU maksimal 100 karakter.',
'sku.unique' => 'SKU sudah digunakan produk lain.',
'description.max' => 'Deskripsi maksimal 5000 karakter.',
'buy_price.required' => 'Harga beli wajib diisi.',
'buy_price.numeric' => 'Harga beli harus berupa angka.',
'buy_price.min' => 'Harga beli tidak boleh negatif.',
'sell_price.required' => 'Harga jual wajib diisi.',
'sell_price.numeric' => 'Harga jual harus berupa angka.',
'sell_price.min' => 'Harga jual tidak boleh negatif.',
'sell_price.gte' => 'Harga jual harus lebih besar atau sama dengan harga beli.',
'stock.required' => 'Stok wajib diisi.',
'stock.integer' => 'Stok harus berupa bilangan bulat.',
'stock.min' => 'Stok tidak boleh negatif.',
'min_stock.required' => 'Minimal stok wajib diisi.',
'min_stock.integer' => 'Minimal stok harus berupa bilangan bulat.',
'min_stock.min' => 'Minimal stok tidak boleh negatif.',
];
}
/**
* Get custom attributes for validator errors.
*/
public function attributes(): array
{
return [
'category_id' => 'kategori',
'name' => 'nama produk',
'sku' => 'SKU',
'description' => 'deskripsi',
'buy_price' => 'harga beli',
'sell_price' => 'harga jual',
'stock' => 'stok',
'min_stock' => 'minimal stok',
'is_active' => 'status aktif',
];
}
}
UpdateProductRequest.php:
<?php
namespace App\\Http\\Requests\\Product;
use Illuminate\\Foundation\\Http\\FormRequest;
use Illuminate\\Validation\\Rule;
class UpdateProductRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return $this->user()->isAdmin();
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'name' => $this->name ? trim($this->name) : null,
'sku' => $this->sku ? strtoupper(trim($this->sku)) : null,
'description' => $this->description ? trim($this->description) : null,
'is_active' => $this->has('is_active') ? $this->boolean('is_active') : null,
]);
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$productId = $this->route('product')->id ?? $this->route('product');
return [
'category_id' => [
'required',
'integer',
Rule::exists('categories', 'id'),
],
'name' => [
'required',
'string',
'max:255',
],
'sku' => [
'required',
'string',
'max:100',
Rule::unique('products', 'sku')->ignore($productId),
],
'description' => [
'nullable',
'string',
'max:5000',
],
'buy_price' => [
'required',
'numeric',
'min:0',
'max:999999999999.99',
],
'sell_price' => [
'required',
'numeric',
'min:0',
'max:999999999999.99',
'gte:buy_price',
],
'stock' => [
'required',
'integer',
'min:0',
],
'min_stock' => [
'required',
'integer',
'min:0',
],
'is_active' => [
'boolean',
],
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'category_id.required' => 'Kategori wajib dipilih.',
'category_id.exists' => 'Kategori yang dipilih tidak valid.',
'name.required' => 'Nama produk wajib diisi.',
'name.max' => 'Nama produk maksimal 255 karakter.',
'sku.required' => 'SKU wajib diisi.',
'sku.max' => 'SKU maksimal 100 karakter.',
'sku.unique' => 'SKU sudah digunakan produk lain.',
'description.max' => 'Deskripsi maksimal 5000 karakter.',
'buy_price.required' => 'Harga beli wajib diisi.',
'buy_price.numeric' => 'Harga beli harus berupa angka.',
'buy_price.min' => 'Harga beli tidak boleh negatif.',
'sell_price.required' => 'Harga jual wajib diisi.',
'sell_price.numeric' => 'Harga jual harus berupa angka.',
'sell_price.min' => 'Harga jual tidak boleh negatif.',
'sell_price.gte' => 'Harga jual harus lebih besar atau sama dengan harga beli.',
'stock.required' => 'Stok wajib diisi.',
'stock.integer' => 'Stok harus berupa bilangan bulat.',
'stock.min' => 'Stok tidak boleh negatif.',
'min_stock.required' => 'Minimal stok wajib diisi.',
'min_stock.integer' => 'Minimal stok harus berupa bilangan bulat.',
'min_stock.min' => 'Minimal stok tidak boleh negatif.',
];
}
}
3. Generate API Resource
Prompt:
Buatkan Laravel API Resource untuk Product:
1. ProductResource - untuk single product
2. ProductCollection - untuk list dengan pagination
Response format sesuai API contract:
- Include semua fields dari model
- Include category relationship (id, name, slug)
- Include computed fields: image_url, profit_margin, profit_percentage, is_low_stock
- Conditional: stock_history hanya di detail view (whenLoaded atau parameter)
Pagination format di collection:
{
"items": [...],
"pagination": {
"current_page": 1,
"last_page": 5,
"per_page": 15,
"total": 75,
"from": 1,
"to": 15,
"has_more_pages": true
}
}
Output:
ProductResource.php:
<?php
namespace App\\Http\\Resources;
use Illuminate\\Http\\Request;
use Illuminate\\Http\\Resources\\Json\\JsonResource;
class ProductResource extends JsonResource
{
/**
* Indicates if additional detail should be included.
*/
protected bool $withDetail = false;
/**
* Set whether to include detailed information.
*/
public function withDetail(bool $value = true): self
{
$this->withDetail = $value;
return $this;
}
/**
* Transform the resource into an array.
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'category_id' => $this->category_id,
'category' => $this->when(
$this->relationLoaded('category'),
fn () => [
'id' => $this->category->id,
'name' => $this->category->name,
'slug' => $this->category->slug,
]
),
'name' => $this->name,
'sku' => $this->sku,
'description' => $this->description,
'image' => $this->image,
'image_url' => $this->image_url,
'buy_price' => (float) $this->buy_price,
'sell_price' => (float) $this->sell_price,
'profit_margin' => (float) $this->profit_margin,
'profit_percentage' => (float) $this->profit_percentage,
'stock' => $this->stock,
'min_stock' => $this->min_stock,
'is_low_stock' => $this->is_low_stock,
'is_active' => $this->is_active,
'created_at' => $this->created_at?->toISOString(),
'updated_at' => $this->updated_at?->toISOString(),
'deleted_at' => $this->when(
$this->deleted_at !== null,
fn () => $this->deleted_at?->toISOString()
),
// Detail-only fields
'stock_history' => $this->when($this->withDetail, fn () => [
'total_in' => $this->transactionInItems()->sum('quantity'),
'total_out' => $this->transactionOutItems()->sum('quantity'),
'last_restock' => $this->transactionInItems()
->latest()
->first()
?->created_at
?->toISOString(),
'last_sale' => $this->transactionOutItems()
->latest()
->first()
?->created_at
?->toISOString(),
]),
];
}
}
ProductCollection.php:
<?php
namespace App\\Http\\Resources;
use Illuminate\\Http\\Request;
use Illuminate\\Http\\Resources\\Json\\ResourceCollection;
class ProductCollection extends ResourceCollection
{
/**
* The resource that this resource collects.
*/
public $collects = ProductResource::class;
/**
* Transform the resource collection into an array.
*/
public function toArray(Request $request): array
{
return [
'items' => $this->collection,
'pagination' => $this->paginationInfo(),
];
}
/**
* Get pagination information.
*/
protected function paginationInfo(): array
{
$paginator = $this->resource;
return [
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
'from' => $paginator->firstItem(),
'to' => $paginator->lastItem(),
'has_more_pages' => $paginator->hasMorePages(),
];
}
}
4. Generate Controller
Prompt:
Buatkan ProductController untuk API dengan fitur:
Endpoints:
1. index - list dengan pagination, search, filter, sort
2. show - detail single product
3. store - create product baru (Admin only)
4. update - update product (Admin only)
5. destroy - soft delete (Admin only)
6. restore - restore soft deleted (Admin only)
Requirements:
- Gunakan Form Request untuk validation
- Gunakan API Resource untuk response
- Response format konsisten: { success, message, data }
- Include try-catch untuk error handling
- Eager load relationships yang diperlukan
- Support query params: search, category_id, is_active, low_stock, sort_by, sort_order, per_page
PHP 8.2+ syntax dengan type hints.
Output:
<?php
namespace App\\Http\\Controllers\\Api\\V1;
use App\\Http\\Controllers\\Controller;
use App\\Http\\Requests\\Product\\StoreProductRequest;
use App\\Http\\Requests\\Product\\UpdateProductRequest;
use App\\Http\\Resources\\ProductCollection;
use App\\Http\\Resources\\ProductResource;
use App\\Models\\Product;
use Illuminate\\Http\\JsonResponse;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Facades\\DB;
use Symfony\\Component\\HttpFoundation\\Response;
class ProductController extends Controller
{
/**
* Display a listing of products.
*/
public function index(Request $request): JsonResponse
{
$perPage = min($request->integer('per_page', 15), 100);
$products = Product::query()
->with('category')
->search($request->input('search'))
->when($request->filled('category_id'), function ($query) use ($request) {
$query->byCategory($request->integer('category_id'));
})
->when($request->has('is_active'), function ($query) use ($request) {
$request->boolean('is_active')
? $query->active()
: $query->inactive();
})
->when($request->boolean('low_stock'), function ($query) {
$query->lowStock();
})
->when($request->boolean('with_trashed'), function ($query) {
$query->withTrashed();
})
->sortBy(
$request->input('sort_by', 'created_at'),
$request->input('sort_order', 'desc')
)
->paginate($perPage);
return $this->successResponse(
'Products retrieved successfully',
new ProductCollection($products)
);
}
/**
* Store a newly created product.
*/
public function store(StoreProductRequest $request): JsonResponse
{
try {
DB::beginTransaction();
$product = Product::create($request->validated());
$product->load('category');
DB::commit();
return $this->successResponse(
'Product created successfully',
new ProductResource($product),
Response::HTTP_CREATED
);
} catch (\\Exception $e) {
DB::rollBack();
return $this->errorResponse(
'Failed to create product',
Response::HTTP_INTERNAL_SERVER_ERROR
);
}
}
/**
* Display the specified product.
*/
public function show(Product $product): JsonResponse
{
$product->load('category');
return $this->successResponse(
'Product retrieved successfully',
(new ProductResource($product))->withDetail()
);
}
/**
* Update the specified product.
*/
public function update(UpdateProductRequest $request, Product $product): JsonResponse
{
try {
DB::beginTransaction();
$product->update($request->validated());
$product->load('category');
DB::commit();
return $this->successResponse(
'Product updated successfully',
new ProductResource($product)
);
} catch (\\Exception $e) {
DB::rollBack();
return $this->errorResponse(
'Failed to update product',
Response::HTTP_INTERNAL_SERVER_ERROR
);
}
}
/**
* Soft delete the specified product.
*/
public function destroy(Product $product): JsonResponse
{
// Check if product has transactions
if (!$product->canBeDeleted()) {
return $this->errorResponse(
'Cannot delete product with existing transactions. Consider deactivating instead.',
Response::HTTP_UNPROCESSABLE_ENTITY,
['product' => ['Product has transaction records']]
);
}
try {
$product->delete();
return $this->successResponse('Product deleted successfully');
} catch (\\Exception $e) {
return $this->errorResponse(
'Failed to delete product',
Response::HTTP_INTERNAL_SERVER_ERROR
);
}
}
/**
* Restore soft deleted product.
*/
public function restore(int $id): JsonResponse
{
$product = Product::onlyTrashed()->findOrFail($id);
try {
$product->restore();
$product->load('category');
return $this->successResponse(
'Product restored successfully',
new ProductResource($product)
);
} catch (\\Exception $e) {
return $this->errorResponse(
'Failed to restore product',
Response::HTTP_INTERNAL_SERVER_ERROR
);
}
}
/**
* Return success response.
*/
protected function successResponse(
string $message,
mixed $data = null,
int $status = Response::HTTP_OK
): JsonResponse {
return response()->json([
'success' => true,
'message' => $message,
'data' => $data,
], $status);
}
/**
* Return error response.
*/
protected function errorResponse(
string $message,
int $status = Response::HTTP_BAD_REQUEST,
?array $errors = null
): JsonResponse {
return response()->json([
'success' => false,
'message' => $message,
'errors' => $errors,
], $status);
}
}
5. Generate Routes
Prompt:
Buatkan route definitions untuk ProductController:
API routes dengan:
- Prefix: api/v1
- Middleware: auth:sanctum
- Resource routes untuk CRUD
- Additional route untuk restore
Format Laravel 11 route syntax.
Output:
<?php
// routes/api.php
use App\\Http\\Controllers\\Api\\V1\\ProductController;
use Illuminate\\Support\\Facades\\Route;
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
// Products
Route::prefix('products')->group(function () {
Route::get('/', [ProductController::class, 'index'])
->name('products.index');
Route::post('/', [ProductController::class, 'store'])
->name('products.store');
Route::get('/{product}', [ProductController::class, 'show'])
->name('products.show');
Route::put('/{product}', [ProductController::class, 'update'])
->name('products.update');
Route::delete('/{product}', [ProductController::class, 'destroy'])
->name('products.destroy');
Route::patch('/{id}/restore', [ProductController::class, 'restore'])
->name('products.restore');
});
});
Atau versi lebih simple dengan apiResource:
<?php
use App\\Http\\Controllers\\Api\\V1\\ProductController;
use Illuminate\\Support\\Facades\\Route;
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
Route::apiResource('products', ProductController::class);
Route::patch('products/{id}/restore', [ProductController::class, 'restore'])
->name('products.restore');
});
Generate untuk Modul Lain
Setelah punya pattern dari Product, untuk modul lain tinggal repeat dengan prompt yang sama.
Prompt Template:
Berdasarkan pattern yang sama dengan modul Product, buatkan scaffolding untuk modul [MODULE_NAME]:
Tabel: [table_name]
Columns: [list columns]
Relationships:
- [list relationships]
Generate:
1. Model dengan relationships, scopes, accessors
2. StoreRequest dan UpdateRequest dengan validation
3. Resource dan Collection untuk response
4. Controller dengan semua CRUD operations
5. Route definitions
Special requirements:
- [any specific requirements]
Gunakan konvensi dan format yang konsisten dengan modul Product.
Tips Scaffolding dengan AI
Batch generate kalau possible
Daripada satu-satu, minta AI generate beberapa files sekaligus. Lebih efficient dan lebih consistent.
Review sebelum pakai
AI bisa generate code yang looks right tapi ada subtle bugs. Always review:
- Typos di column names
- Logic errors di scopes
- Missing imports
- Wrong relationship types
Customize base controller
Kalau response format sama di semua controllers, buat base controller:
Buatkan ApiController base class dengan:
- successResponse() method
- errorResponse() method
- paginatedResponse() method
Semua API controllers extend dari class ini.
Generate Factory dan Seeder juga
Berdasarkan model Product, buatkan:
1. ProductFactory dengan fake data yang realistic
2. ProductSeeder untuk seed sample data (10 categories, 50 products)
Checklist Scaffolding
Sebelum lanjut ke business logic:
- [ ] Model dengan proper relationships dan scopes
- [ ] Form Requests dengan validation lengkap
- [ ] API Resources dengan format yang konsisten
- [ ] Controller dengan semua CRUD methods
- [ ] Routes terdefinisi dan bisa diakses
- [ ] Basic test dengan Postman/Insomnia — endpoints respond correctly
Dengan scaffolding yang solid, sekarang tinggal focus ke business logic yang lebih kompleks. That's next!
Bagian 6: Implementasi Business Logic yang Kompleks
Scaffolding udah beres. CRUD basic udah jalan. Sekarang masuk ke bagian yang sebenarnya challenging: business logic yang kompleks.
Ini area dimana banyak freelancer struggle:
- Logic yang melibatkan multiple tables
- Kalkulasi yang rumit
- Conditional flows yang banyak cabang
- Edge cases yang tricky
Dan ini juga area dimana AI bisa sangat membantu — bukan untuk replace thinking kalian, tapi untuk brainstorm approach dan generate initial implementation.
Kapan Business Logic Dianggap "Kompleks"?
Simple logic:
- CRUD operations
- Basic filtering dan sorting
- Simple calculations (total = price × quantity)
Complex logic:
- Multi-step transactions dengan rollback
- Calculations yang melibatkan historical data
- State machines (status flow dengan rules)
- Inventory management (FIFO, LIFO, average costing)
- Financial calculations (profit, margin, dengan berbagai metode)
- Concurrent operations (race conditions)
Di inventory app kita, beberapa complex logic yang perlu diimplementasi:
- Create transaction dengan multiple items + stock validation + stock reduction
- Calculate profit dengan metode FIFO
- Generate reports dengan aggregations
Let's tackle satu-satu dengan bantuan AI.
Case 1: Create Sale Transaction
Ini flow yang udah kita design di Bagian 4. Sekarang implementasinya.
Requirements:
- Transaction bisa punya multiple items
- Setiap item harus validate stok dulu
- Stok berkurang setelah transaction confirmed
- Semua dalam satu database transaction (atomic)
- Generate invoice number otomatis
- Log activity
Prompt:
Saya perlu implement fitur Create Sale Transaction di Laravel 11.
Context:
- Tabel transactions_out: id, user_id, invoice_number, transaction_date, subtotal, discount, total_amount, notes
- Tabel transaction_out_items: id, transaction_out_id, product_id, quantity, price, subtotal
- Tabel products: punya kolom stock
Business Rules:
1. Transaction harus punya minimal 1 item
2. Setiap item harus validate: product exists, product active, stock mencukupi
3. Harga yang disimpan adalah harga saat transaksi (snapshot), bukan reference
4. Subtotal per item = quantity × price
5. Total transaksi = sum(subtotal items) - discount
6. Setelah transaction created, kurangi stock untuk setiap product
7. Invoice number format: INV-OUT-YYYYMMDD-XXXX (sequential per hari)
8. Semua harus atomic — kalau ada yang gagal, rollback semua
Tolong buatkan:
1. Service class untuk handle business logic
2. Form Request untuk validation
3. Jelaskan approach/algorithm yang dipakai
4. Handle edge cases: concurrent transactions, stock berubah di tengah proses
Gunakan PHP 8.2+ syntax dengan proper error handling.
Output dari Claude:
Approach/Algorithm
1. VALIDATION PHASE
├── Validate basic request data (items not empty, discount valid)
├── For each item:
│ ├── Check product exists dan active
│ ├── Check stock availability
│ └── Get current sell_price
└── If any validation fails → return errors
2. PREPARATION PHASE
├── Calculate subtotals per item
├── Calculate transaction subtotal
├── Apply discount
├── Calculate final total
└── Generate invoice number
3. EXECUTION PHASE (dalam DB Transaction)
├── Lock products (FOR UPDATE) untuk prevent race condition
├── Re-validate stock (bisa berubah sejak validation phase)
├── Create transaction record
├── Create transaction items
├── Reduce stock untuk setiap product
├── Log activity
└── Commit atau rollback
TransactionOutService.php
<?php
namespace App\\Services;
use App\\Models\\Product;
use App\\Models\\TransactionOut;
use App\\Models\\TransactionOutItem;
use Illuminate\\Support\\Collection;
use Illuminate\\Support\\Facades\\DB;
use Illuminate\\Support\\Facades\\Log;
use App\\Exceptions\\InsufficientStockException;
use App\\Exceptions\\ProductNotAvailableException;
use App\\Events\\TransactionCreated;
class TransactionOutService
{
/**
* Create a new sale transaction.
*
* @param array $data Transaction data
* @param int $userId User creating the transaction
* @return TransactionOut
* @throws InsufficientStockException
* @throws ProductNotAvailableException
* @throws \\Exception
*/
public function createTransaction(array $data, int $userId): TransactionOut
{
// Phase 1: Validate dan prepare items
$preparedItems = $this->prepareItems($data['items']);
// Phase 2: Calculate totals
$subtotal = $preparedItems->sum('subtotal');
$discount = (float) ($data['discount'] ?? 0);
$total = $subtotal - $discount;
if ($total < 0) {
throw new \\InvalidArgumentException('Discount cannot exceed subtotal');
}
// Phase 3: Execute dalam transaction
return DB::transaction(function () use ($data, $userId, $preparedItems, $subtotal, $discount, $total) {
// Lock products untuk prevent race condition
$productIds = $preparedItems->pluck('product_id')->toArray();
$products = Product::whereIn('id', $productIds)
->lockForUpdate()
->get()
->keyBy('id');
// Re-validate stock setelah lock
foreach ($preparedItems as $item) {
$product = $products->get($item['product_id']);
if (!$product || !$product->is_active) {
throw new ProductNotAvailableException(
"Product {$item['product_name']} is no longer available"
);
}
if ($product->stock < $item['quantity']) {
throw new InsufficientStockException(
"Insufficient stock for {$product->name}. Available: {$product->stock}, Requested: {$item['quantity']}"
);
}
}
// Create transaction
$transaction = TransactionOut::create([
'user_id' => $userId,
'invoice_number' => $this->generateInvoiceNumber(),
'transaction_date' => $data['transaction_date'] ?? now()->toDateString(),
'subtotal' => $subtotal,
'discount' => $discount,
'total_amount' => $total,
'notes' => $data['notes'] ?? null,
]);
// Create items dan reduce stock
foreach ($preparedItems as $item) {
TransactionOutItem::create([
'transaction_out_id' => $transaction->id,
'product_id' => $item['product_id'],
'quantity' => $item['quantity'],
'price' => $item['price'],
'subtotal' => $item['subtotal'],
]);
// Reduce stock
$product = $products->get($item['product_id']);
$product->decrement('stock', $item['quantity']);
}
// Load relationships untuk response
$transaction->load(['items.product', 'user']);
// Dispatch event untuk activity log, notifications, dll
event(new TransactionCreated($transaction));
return $transaction;
});
}
/**
* Prepare dan validate items.
*
* @param array $items Raw items from request
* @return Collection Prepared items dengan price dan subtotal
*/
protected function prepareItems(array $items): Collection
{
$prepared = collect();
foreach ($items as $item) {
$product = Product::find($item['product_id']);
if (!$product) {
throw new ProductNotAvailableException(
"Product with ID {$item['product_id']} not found"
);
}
if (!$product->is_active) {
throw new ProductNotAvailableException(
"Product {$product->name} is not active"
);
}
if ($product->stock < $item['quantity']) {
throw new InsufficientStockException(
"Insufficient stock for {$product->name}. Available: {$product->stock}, Requested: {$item['quantity']}"
);
}
$prepared->push([
'product_id' => $product->id,
'product_name' => $product->name,
'quantity' => (int) $item['quantity'],
'price' => $product->sell_price, // Snapshot current price
'subtotal' => $product->sell_price * $item['quantity'],
]);
}
return $prepared;
}
/**
* Generate unique invoice number.
* Format: INV-OUT-YYYYMMDD-XXXX
*/
protected function generateInvoiceNumber(): string
{
$date = now()->format('Ymd');
$prefix = "INV-OUT-{$date}-";
// Get last invoice number for today
$lastInvoice = TransactionOut::where('invoice_number', 'like', "{$prefix}%")
->orderBy('invoice_number', 'desc')
->lockForUpdate() // Prevent duplicate in concurrent requests
->first();
if ($lastInvoice) {
$lastNumber = (int) substr($lastInvoice->invoice_number, -4);
$newNumber = $lastNumber + 1;
} else {
$newNumber = 1;
}
return $prefix . str_pad($newNumber, 4, '0', STR_PAD_LEFT);
}
/**
* Void/cancel a transaction.
* Restore stock dan soft delete.
*/
public function voidTransaction(TransactionOut $transaction): TransactionOut
{
if ($transaction->trashed()) {
throw new \\Exception('Transaction is already voided');
}
return DB::transaction(function () use ($transaction) {
// Lock products
$productIds = $transaction->items->pluck('product_id')->toArray();
$products = Product::whereIn('id', $productIds)
->lockForUpdate()
->get()
->keyBy('id');
// Restore stock
foreach ($transaction->items as $item) {
$product = $products->get($item->product_id);
if ($product) {
$product->increment('stock', $item->quantity);
}
}
// Soft delete transaction
$transaction->delete();
return $transaction->fresh();
});
}
}
Custom Exceptions
InsufficientStockException.php:
<?php
namespace App\\Exceptions;
use Exception;
class InsufficientStockException extends Exception
{
protected $code = 422;
}
ProductNotAvailableException.php:
<?php
namespace App\\Exceptions;
use Exception;
class ProductNotAvailableException extends Exception
{
protected $code = 422;
}
StoreTransactionOutRequest.php
<?php
namespace App\\Http\\Requests\\TransactionOut;
use Illuminate\\Foundation\\Http\\FormRequest;
use Illuminate\\Validation\\Rule;
class StoreTransactionOutRequest extends FormRequest
{
public function authorize(): bool
{
return true; // Staff dan Admin bisa create
}
public function rules(): array
{
return [
'transaction_date' => [
'nullable',
'date',
'before_or_equal:today',
],
'discount' => [
'nullable',
'numeric',
'min:0',
],
'notes' => [
'nullable',
'string',
'max:1000',
],
'items' => [
'required',
'array',
'min:1',
],
'items.*.product_id' => [
'required',
'integer',
Rule::exists('products', 'id')->where(function ($query) {
$query->whereNull('deleted_at');
}),
],
'items.*.quantity' => [
'required',
'integer',
'min:1',
],
];
}
public function messages(): array
{
return [
'items.required' => 'Minimal harus ada 1 item.',
'items.min' => 'Minimal harus ada 1 item.',
'items.*.product_id.required' => 'Product harus dipilih.',
'items.*.product_id.exists' => 'Product tidak ditemukan.',
'items.*.quantity.required' => 'Quantity harus diisi.',
'items.*.quantity.min' => 'Quantity minimal 1.',
'discount.min' => 'Discount tidak boleh negatif.',
'transaction_date.before_or_equal' => 'Tanggal transaksi tidak boleh di masa depan.',
];
}
/**
* Additional validation setelah basic rules pass.
*/
public function withValidator($validator): void
{
$validator->after(function ($validator) {
// Check for duplicate products
if ($this->has('items')) {
$productIds = collect($this->input('items'))->pluck('product_id');
$duplicates = $productIds->duplicates();
if ($duplicates->isNotEmpty()) {
$validator->errors()->add(
'items',
'Tidak boleh ada product yang sama. Gabungkan quantity-nya.'
);
}
}
});
}
}
Controller Method
// Di TransactionOutController.php
public function store(StoreTransactionOutRequest $request): JsonResponse
{
try {
$transaction = $this->transactionService->createTransaction(
$request->validated(),
$request->user()->id
);
return $this->successResponse(
'Transaction created successfully',
new TransactionOutResource($transaction),
Response::HTTP_CREATED
);
} catch (InsufficientStockException $e) {
return $this->errorResponse(
$e->getMessage(),
Response::HTTP_UNPROCESSABLE_ENTITY,
['stock' => [$e->getMessage()]]
);
} catch (ProductNotAvailableException $e) {
return $this->errorResponse(
$e->getMessage(),
Response::HTTP_UNPROCESSABLE_ENTITY,
['product' => [$e->getMessage()]]
);
} catch (\\Exception $e) {
Log::error('Transaction creation failed', [
'error' => $e->getMessage(),
'user_id' => $request->user()->id,
'data' => $request->validated(),
]);
return $this->errorResponse(
'Failed to create transaction. Please try again.',
Response::HTTP_INTERNAL_SERVER_ERROR
);
}
}
Case 2: Calculate Profit dengan FIFO
Ini lebih kompleks. Untuk hitung profit yang akurat, kita perlu tau harga beli dari batch mana untuk setiap item yang terjual.
Prompt:
Saya perlu implement fitur Calculate Profit dengan metode FIFO (First In First Out) untuk inventory app.
Context:
- transactions_in: pembelian dari supplier, punya items dengan quantity dan buy_price
- transactions_out: penjualan, punya items dengan quantity dan sell_price
- Setiap pembelian bisa punya harga berbeda (batch berbeda)
- Saat menghitung profit penjualan, harus pakai harga beli dari batch yang masuk duluan (FIFO)
Contoh:
- Beli 10 unit @ Rp 100 (Batch 1, Jan 1)
- Beli 10 unit @ Rp 120 (Batch 2, Jan 5)
- Jual 15 unit @ Rp 150
Profit = (10 × (150-100)) + (5 × (150-120)) = 500 + 150 = Rp 650
(10 unit dari Batch 1, 5 unit dari Batch 2)
Buatkan:
1. Service class untuk calculate profit per transaction dan per periode
2. Jelaskan algorithm/approach
3. Handle edge cases: stock dari batch habis, mixed batches
Note: Untuk simplicity, kita bisa track di level product (bukan serial number).
Output:
Algorithm FIFO Costing
Untuk setiap product yang terjual:
1. Get all purchase batches (transactions_in items) untuk product tersebut
- Ordered by transaction_date ASC (FIFO)
2. Track "remaining quantity" untuk setiap batch
- Initially = purchased quantity
3. Untuk setiap sale of this product:
- Allocate dari batch yang paling lama dulu
- Jika batch habis, lanjut ke batch berikutnya
- Calculate COGS = allocated_qty × batch_buy_price
4. Profit = Sale Amount - COGS
ProfitCalculationService.php
<?php
namespace App\\Services;
use App\\Models\\Product;
use App\\Models\\TransactionOut;
use App\\Models\\TransactionOutItem;
use App\\Models\\TransactionInItem;
use Illuminate\\Support\\Collection;
use Illuminate\\Support\\Facades\\DB;
use Carbon\\Carbon;
class ProfitCalculationService
{
/**
* Calculate profit untuk single transaction.
*/
public function calculateTransactionProfit(TransactionOut $transaction): array
{
$totalRevenue = 0;
$totalCOGS = 0;
$itemDetails = [];
foreach ($transaction->items as $item) {
$cogs = $this->calculateItemCOGS(
$item->product_id,
$item->quantity,
$transaction->transaction_date
);
$revenue = $item->subtotal;
$profit = $revenue - $cogs;
$totalRevenue += $revenue;
$totalCOGS += $cogs;
$itemDetails[] = [
'product_id' => $item->product_id,
'product_name' => $item->product->name,
'quantity' => $item->quantity,
'sell_price' => $item->price,
'revenue' => $revenue,
'cogs' => $cogs,
'profit' => $profit,
'margin_percentage' => $revenue > 0
? round(($profit / $revenue) * 100, 2)
: 0,
];
}
return [
'transaction_id' => $transaction->id,
'invoice_number' => $transaction->invoice_number,
'transaction_date' => $transaction->transaction_date,
'total_revenue' => $totalRevenue,
'total_cogs' => $totalCOGS,
'gross_profit' => $totalRevenue - $totalCOGS,
'discount' => $transaction->discount,
'net_profit' => $totalRevenue - $totalCOGS - $transaction->discount,
'margin_percentage' => $totalRevenue > 0
? round((($totalRevenue - $totalCOGS) / $totalRevenue) * 100, 2)
: 0,
'items' => $itemDetails,
];
}
/**
* Calculate COGS untuk single item menggunakan FIFO.
*
* @param int $productId
* @param int $quantity Quantity yang dijual
* @param string|Carbon $saleDate Tanggal penjualan (untuk determine batch mana yang available)
* @return float Total COGS
*/
protected function calculateItemCOGS(int $productId, int $quantity, $saleDate): float
{
// Get semua batches (purchases) untuk product ini sebelum sale date
// Ordered by date ASC (FIFO - First In First Out)
$batches = $this->getAvailableBatches($productId, $saleDate);
$remainingQty = $quantity;
$totalCOGS = 0;
foreach ($batches as $batch) {
if ($remainingQty <= 0) {
break;
}
// Calculate remaining in this batch (purchased - already sold)
$batchRemaining = $batch->remaining_quantity;
if ($batchRemaining <= 0) {
continue;
}
// Allocate from this batch
$allocateQty = min($remainingQty, $batchRemaining);
$totalCOGS += $allocateQty * $batch->price;
$remainingQty -= $allocateQty;
}
// Jika masih ada remaining (edge case: stock tidak cukup dari purchases)
// Use average cost atau last known cost sebagai fallback
if ($remainingQty > 0) {
$fallbackPrice = $this->getFallbackCost($productId);
$totalCOGS += $remainingQty * $fallbackPrice;
}
return $totalCOGS;
}
/**
* Get available batches dengan remaining quantity.
*/
protected function getAvailableBatches(int $productId, $beforeDate): Collection
{
// Subquery untuk hitung total sold per batch
$soldSubquery = DB::table('transaction_out_items')
->join('transactions_out', 'transactions_out.id', '=', 'transaction_out_items.transaction_out_id')
->select('transaction_out_items.product_id')
->selectRaw('SUM(transaction_out_items.quantity) as total_sold')
->whereNull('transactions_out.deleted_at')
->where('transactions_out.transaction_date', '<=', $beforeDate)
->groupBy('transaction_out_items.product_id');
// Get purchase batches dengan running total
return TransactionInItem::query()
->join('transactions_in', 'transactions_in.id', '=', 'transaction_in_items.transaction_in_id')
->where('transaction_in_items.product_id', $productId)
->where('transactions_in.transaction_date', '<=', $beforeDate)
->whereNull('transactions_in.deleted_at')
->orderBy('transactions_in.transaction_date', 'asc')
->orderBy('transactions_in.id', 'asc')
->select([
'transaction_in_items.*',
'transactions_in.transaction_date',
])
->get()
->map(function ($batch) use ($productId, $beforeDate) {
// Calculate remaining untuk batch ini (simplified)
// Untuk accuracy yang lebih tinggi, perlu tracking per-batch allocation
$batch->remaining_quantity = $this->calculateBatchRemaining(
$batch,
$productId,
$beforeDate
);
return $batch;
});
}
/**
* Calculate remaining quantity in a batch.
* Simplified version - untuk production mungkin perlu batch tracking table.
*/
protected function calculateBatchRemaining($batch, int $productId, $beforeDate): int
{
// Ini simplified approach
// Untuk accurate FIFO, idealnya ada tabel terpisah untuk track allocation
// Get total purchased sampai batch ini
$totalPurchased = TransactionInItem::query()
->join('transactions_in', 'transactions_in.id', '=', 'transaction_in_items.transaction_in_id')
->where('transaction_in_items.product_id', $productId)
->where('transactions_in.transaction_date', '<=', $batch->transaction_date)
->where(function ($q) use ($batch) {
$q->where('transactions_in.transaction_date', '<', $batch->transaction_date)
->orWhere(function ($q2) use ($batch) {
$q2->where('transactions_in.transaction_date', '=', $batch->transaction_date)
->where('transactions_in.id', '<=', $batch->transaction_in_id);
});
})
->whereNull('transactions_in.deleted_at')
->sum('transaction_in_items.quantity');
// Get total sold sampai before date
$totalSold = TransactionOutItem::query()
->join('transactions_out', 'transactions_out.id', '=', 'transaction_out_items.transaction_out_id')
->where('transaction_out_items.product_id', $productId)
->where('transactions_out.transaction_date', '<=', $beforeDate)
->whereNull('transactions_out.deleted_at')
->sum('transaction_out_items.quantity');
// Calculate cumulative remaining
$cumulativeRemaining = $totalPurchased - $totalSold;
// This batch's remaining adalah min dari batch quantity dan cumulative remaining
return max(0, min($batch->quantity, $cumulativeRemaining));
}
/**
* Get fallback cost (average atau last purchase price).
*/
protected function getFallbackCost(int $productId): float
{
// Option 1: Use average purchase price
$avgPrice = TransactionInItem::where('product_id', $productId)
->avg('price');
if ($avgPrice) {
return $avgPrice;
}
// Option 2: Use product's buy_price
$product = Product::find($productId);
return $product?->buy_price ?? 0;
}
/**
* Calculate profit untuk periode tertentu.
*/
public function calculatePeriodProfit(Carbon $startDate, Carbon $endDate): array
{
$transactions = TransactionOut::with(['items.product'])
->whereBetween('transaction_date', [$startDate, $endDate])
->orderBy('transaction_date')
->get();
$totalRevenue = 0;
$totalCOGS = 0;
$totalDiscount = 0;
$transactionDetails = [];
foreach ($transactions as $transaction) {
$profit = $this->calculateTransactionProfit($transaction);
$totalRevenue += $profit['total_revenue'];
$totalCOGS += $profit['total_cogs'];
$totalDiscount += $profit['discount'];
$transactionDetails[] = $profit;
}
return [
'period' => [
'start' => $startDate->toDateString(),
'end' => $endDate->toDateString(),
],
'summary' => [
'total_transactions' => $transactions->count(),
'total_revenue' => $totalRevenue,
'total_cogs' => $totalCOGS,
'gross_profit' => $totalRevenue - $totalCOGS,
'total_discount' => $totalDiscount,
'net_profit' => $totalRevenue - $totalCOGS - $totalDiscount,
'margin_percentage' => $totalRevenue > 0
? round((($totalRevenue - $totalCOGS) / $totalRevenue) * 100, 2)
: 0,
],
'transactions' => $transactionDetails,
];
}
/**
* Calculate profit per product untuk periode tertentu.
*/
public function calculateProductProfit(Carbon $startDate, Carbon $endDate): Collection
{
return TransactionOutItem::query()
->join('transactions_out', 'transactions_out.id', '=', 'transaction_out_items.transaction_out_id')
->join('products', 'products.id', '=', 'transaction_out_items.product_id')
->whereBetween('transactions_out.transaction_date', [$startDate, $endDate])
->whereNull('transactions_out.deleted_at')
->select('products.id', 'products.name', 'products.sku')
->selectRaw('SUM(transaction_out_items.quantity) as total_quantity')
->selectRaw('SUM(transaction_out_items.subtotal) as total_revenue')
->groupBy('products.id', 'products.name', 'products.sku')
->orderByDesc('total_revenue')
->get()
->map(function ($item) use ($startDate, $endDate) {
// Calculate COGS untuk product ini
$cogs = $this->calculateProductCOGS(
$item->id,
$item->total_quantity,
$endDate
);
$item->total_cogs = $cogs;
$item->gross_profit = $item->total_revenue - $cogs;
$item->margin_percentage = $item->total_revenue > 0
? round(($item->gross_profit / $item->total_revenue) * 100, 2)
: 0;
return $item;
});
}
/**
* Calculate total COGS untuk product dalam periode.
*/
protected function calculateProductCOGS(int $productId, int $totalQuantity, $endDate): float
{
return $this->calculateItemCOGS($productId, $totalQuantity, $endDate);
}
}
Case 3: Generate Report dengan Complex Aggregations
Prompt:
Buatkan ReportService untuk inventory app dengan method:
1. getDashboardSummary() - Summary untuk dashboard
- Total sales hari ini, minggu ini, bulan ini
- Total profit (sudah pakai FIFO)
- Top 5 selling products
- Low stock alerts
- Recent transactions
2. getStockReport() - Laporan stok
- Current stock per product
- Stock value (quantity × buy_price)
- Stock movement (in/out) per periode
3. getSalesReport() - Laporan penjualan
- Sales per periode (daily, weekly, monthly)
- Group by category
- Comparison dengan periode sebelumnya
Gunakan Query Builder yang efficient dan avoid N+1 queries.
Output (partial - getDashboardSummary):
<?php
namespace App\\Services;
use App\\Models\\Product;
use App\\Models\\TransactionOut;
use App\\Models\\TransactionIn;
use Illuminate\\Support\\Facades\\DB;
use Carbon\\Carbon;
class ReportService
{
protected ProfitCalculationService $profitService;
public function __construct(ProfitCalculationService $profitService)
{
$this->profitService = $profitService;
}
/**
* Get dashboard summary.
*/
public function getDashboardSummary(): array
{
$today = Carbon::today();
$startOfWeek = Carbon::now()->startOfWeek();
$startOfMonth = Carbon::now()->startOfMonth();
return [
'sales' => [
'today' => $this->getSalesTotal($today, $today),
'this_week' => $this->getSalesTotal($startOfWeek, $today),
'this_month' => $this->getSalesTotal($startOfMonth, $today),
],
'profit' => [
'today' => $this->getProfitTotal($today, $today),
'this_week' => $this->getProfitTotal($startOfWeek, $today),
'this_month' => $this->getProfitTotal($startOfMonth, $today),
],
'transactions_count' => [
'today' => $this->getTransactionCount($today, $today),
'this_week' => $this->getTransactionCount($startOfWeek, $today),
'this_month' => $this->getTransactionCount($startOfMonth, $today),
],
'top_products' => $this->getTopSellingProducts(5, $startOfMonth, $today),
'low_stock_alerts' => $this->getLowStockProducts(10),
'recent_transactions' => $this->getRecentTransactions(5),
];
}
protected function getSalesTotal(Carbon $start, Carbon $end): float
{
return TransactionOut::whereBetween('transaction_date', [$start, $end])
->sum('total_amount');
}
protected function getProfitTotal(Carbon $start, Carbon $end): float
{
$report = $this->profitService->calculatePeriodProfit($start, $end);
return $report['summary']['net_profit'];
}
protected function getTransactionCount(Carbon $start, Carbon $end): int
{
return TransactionOut::whereBetween('transaction_date', [$start, $end])
->count();
}
protected function getTopSellingProducts(int $limit, Carbon $start, Carbon $end): array
{
return DB::table('transaction_out_items')
->join('transactions_out', 'transactions_out.id', '=', 'transaction_out_items.transaction_out_id')
->join('products', 'products.id', '=', 'transaction_out_items.product_id')
->whereBetween('transactions_out.transaction_date', [$start, $end])
->whereNull('transactions_out.deleted_at')
->select(
'products.id',
'products.name',
'products.sku',
DB::raw('SUM(transaction_out_items.quantity) as total_sold'),
DB::raw('SUM(transaction_out_items.subtotal) as total_revenue')
)
->groupBy('products.id', 'products.name', 'products.sku')
->orderByDesc('total_sold')
->limit($limit)
->get()
->toArray();
}
protected function getLowStockProducts(int $limit): array
{
return Product::query()
->active()
->lowStock()
->select('id', 'name', 'sku', 'stock', 'min_stock')
->orderBy('stock')
->limit($limit)
->get()
->toArray();
}
protected function getRecentTransactions(int $limit): array
{
return TransactionOut::with(['user:id,name', 'items'])
->latest('transaction_date')
->limit($limit)
->get()
->map(fn ($t) => [
'id' => $t->id,
'invoice_number' => $t->invoice_number,
'transaction_date' => $t->transaction_date,
'total_amount' => $t->total_amount,
'items_count' => $t->items->count(),
'created_by' => $t->user->name,
])
->toArray();
}
}
Tips Business Logic dengan AI
Jelaskan business context dengan detail
Semakin detail context yang dikasih, semakin accurate solution yang di-generate. Include:
- Business rules
- Edge cases yang kalian tau
- Constraints (performance, data volume, dll)
Minta explain algorithm dulu
Sebelum minta code, minta AI explain approach-nya. Ini membantu kalian validate apakah logic-nya make sense.
Iterasi untuk edge cases
Code sudah bagus, tapi tolong handle edge case berikut:
1. Bagaimana kalau ada transaction yang di-void? Stock harus di-restore
2. Bagaimana kalau purchase batch di-edit setelah ada penjualan?
Test dengan data realistic
Code dari AI perlu di-test dengan berbagai scenarios:
- Happy path
- Edge cases
- Large data volume
- Concurrent requests
Checklist Business Logic
- [ ] Algorithm udah di-explain dan make sense
- [ ] Code handles semua known edge cases
- [ ] Database transactions dipakai untuk atomic operations
- [ ] Proper locking untuk prevent race conditions
- [ ] Error handling comprehensive
- [ ] Tested dengan berbagai scenarios
Business logic yang solid adalah foundation dari app yang reliable. Next, kita bahas gimana cleanup code yang udah jadi dengan refactoring.
Bagian 7: Refactoring Code — Clean Up dan Optimization
Projek udah jalan. Fitur-fitur udah works. Tapi... kode-nya berantakan.
Ini realita yang sering terjadi, especially pas deadline kejar:
- Controller gemuk dengan ratusan baris
- Logic duplikat di beberapa tempat
- Query N+1 yang bikin lambat
- Hardcoded values dimana-mana
- Nggak ada yang berani refactor karena takut break something
Sound familiar?
Refactoring itu necessary evil. Tapi dengan AI, prosesnya bisa jauh lebih cepat dan less painful.
Kapan Harus Refactor?
Red flags yang indicate perlu refactor:
- Fat Controllers — Controller lebih dari 200-300 baris
- Duplicate Code — Logic yang sama di 2+ tempat
- God Classes — Satu class yang handle terlalu banyak responsibility
- Long Methods — Method lebih dari 30-50 baris
- Deep Nesting — If dalam if dalam if dalam if...
- N+1 Queries — Performance issue karena query dalam loop
- Magic Numbers/Strings — Hardcoded values tanpa context
- Tight Coupling — Class yang terlalu dependent ke class lain
Gimana AI Membantu Refactoring
AI bisa:
- Identify code smells dari code yang di-paste
- Suggest refactoring patterns yang appropriate
- Extract logic ke Service/Action classes
- Optimize queries
- Apply SOLID principles
- Generate refactored version
Yang penting: AI perlu context. Jangan cuma paste code, kasih tau juga apa yang mau di-achieve.
Case 1: Refactor Fat Controller
Ini contoh classic. Controller yang jadi tempat semua logic.
Code Sebelum (Bloated Controller):
<?php
namespace App\\Http\\Controllers\\Api\\V1;
use App\\Http\\Controllers\\Controller;
use App\\Models\\Product;
use App\\Models\\TransactionOut;
use App\\Models\\TransactionOutItem;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Facades\\DB;
use Illuminate\\Support\\Facades\\Mail;
use Illuminate\\Support\\Facades\\Log;
class TransactionController extends Controller
{
public function store(Request $request)
{
// Validation
$validated = $request->validate([
'customer_name' => 'nullable|string|max:255',
'items' => 'required|array|min:1',
'items.*.product_id' => 'required|exists:products,id',
'items.*.quantity' => 'required|integer|min:1',
'discount' => 'nullable|numeric|min:0',
'notes' => 'nullable|string',
]);
// Check stock availability
$stockErrors = [];
foreach ($validated['items'] as $index => $item) {
$product = Product::find($item['product_id']);
if (!$product) {
$stockErrors["items.{$index}.product_id"] = ["Product not found"];
continue;
}
if (!$product->is_active) {
$stockErrors["items.{$index}.product_id"] = ["Product {$product->name} is not active"];
continue;
}
if ($product->stock < $item['quantity']) {
$stockErrors["items.{$index}.quantity"] = [
"Insufficient stock for {$product->name}. Available: {$product->stock}"
];
}
}
if (!empty($stockErrors)) {
return response()->json([
'success' => false,
'message' => 'Validation failed',
'errors' => $stockErrors,
], 422);
}
// Calculate totals
$subtotal = 0;
$itemsData = [];
foreach ($validated['items'] as $item) {
$product = Product::find($item['product_id']);
$itemSubtotal = $product->sell_price * $item['quantity'];
$subtotal += $itemSubtotal;
$itemsData[] = [
'product_id' => $product->id,
'product_name' => $product->name,
'quantity' => $item['quantity'],
'price' => $product->sell_price,
'subtotal' => $itemSubtotal,
];
}
$discount = $validated['discount'] ?? 0;
if ($discount > $subtotal) {
return response()->json([
'success' => false,
'message' => 'Discount cannot exceed subtotal',
'errors' => ['discount' => ['Discount cannot exceed subtotal']],
], 422);
}
$total = $subtotal - $discount;
// Generate invoice number
$today = now()->format('Ymd');
$prefix = "INV-{$today}-";
$lastInvoice = TransactionOut::where('invoice_number', 'like', "{$prefix}%")
->orderBy('invoice_number', 'desc')
->first();
if ($lastInvoice) {
$lastNumber = (int) substr($lastInvoice->invoice_number, -4);
$newNumber = str_pad($lastNumber + 1, 4, '0', STR_PAD_LEFT);
} else {
$newNumber = '0001';
}
$invoiceNumber = $prefix . $newNumber;
// Create transaction
DB::beginTransaction();
try {
$transaction = TransactionOut::create([
'user_id' => auth()->id(),
'invoice_number' => $invoiceNumber,
'transaction_date' => now()->toDateString(),
'customer_name' => $validated['customer_name'] ?? null,
'subtotal' => $subtotal,
'discount' => $discount,
'total_amount' => $total,
'notes' => $validated['notes'] ?? null,
]);
foreach ($itemsData as $item) {
TransactionOutItem::create([
'transaction_out_id' => $transaction->id,
'product_id' => $item['product_id'],
'quantity' => $item['quantity'],
'price' => $item['price'],
'subtotal' => $item['subtotal'],
]);
// Reduce stock
Product::where('id', $item['product_id'])
->decrement('stock', $item['quantity']);
}
// Log activity
DB::table('activity_logs')->insert([
'user_id' => auth()->id(),
'action' => 'created',
'model_type' => TransactionOut::class,
'model_id' => $transaction->id,
'new_values' => json_encode($transaction->toArray()),
'ip_address' => $request->ip(),
'created_at' => now(),
'updated_at' => now(),
]);
DB::commit();
// Send notification email
if ($validated['customer_name'] && isset($validated['customer_email'])) {
try {
Mail::send('emails.receipt', [
'transaction' => $transaction,
'items' => $itemsData,
], function ($message) use ($validated) {
$message->to($validated['customer_email'])
->subject('Transaction Receipt');
});
} catch (\\Exception $e) {
Log::warning('Failed to send receipt email', [
'transaction_id' => $transaction->id,
'error' => $e->getMessage(),
]);
}
}
// Check low stock alerts
foreach ($itemsData as $item) {
$product = Product::find($item['product_id']);
if ($product->stock <= $product->min_stock) {
// Send low stock alert
Log::warning('Low stock alert', [
'product_id' => $product->id,
'product_name' => $product->name,
'current_stock' => $product->stock,
'min_stock' => $product->min_stock,
]);
// Could also send notification to admin here
}
}
$transaction->load('items.product');
return response()->json([
'success' => true,
'message' => 'Transaction created successfully',
'data' => [
'id' => $transaction->id,
'invoice_number' => $transaction->invoice_number,
'transaction_date' => $transaction->transaction_date,
'customer_name' => $transaction->customer_name,
'subtotal' => $transaction->subtotal,
'discount' => $transaction->discount,
'total_amount' => $transaction->total_amount,
'items' => $transaction->items->map(function ($item) {
return [
'product_id' => $item->product_id,
'product_name' => $item->product->name,
'quantity' => $item->quantity,
'price' => $item->price,
'subtotal' => $item->subtotal,
];
}),
],
], 201);
} catch (\\Exception $e) {
DB::rollBack();
Log::error('Transaction creation failed', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'user_id' => auth()->id(),
'data' => $validated,
]);
return response()->json([
'success' => false,
'message' => 'Failed to create transaction',
'errors' => null,
], 500);
}
}
}
150+ baris dalam satu method. This is a mess.
Prompt untuk Refactor:
Refactor controller method berikut yang terlalu gemuk (150+ baris):
[paste code di atas]
Tolong refactor dengan approach:
1. Extract validation ke Form Request class
2. Extract business logic ke Service class (CreateTransactionService atau TransactionService)
3. Extract invoice number generation ke helper atau trait
4. Extract notification/email ke Event + Listener atau Job
5. Extract activity logging ke Model Observer atau trait
6. Extract response formatting ke API Resource
Apply principles:
- Single Responsibility Principle
- Dependency Injection
- Thin Controllers
- Fat Models / Services
Output yang diharapkan:
1. Refactored Controller (should be < 30 lines untuk method store)
2. Form Request class
3. Service class dengan business logic
4. Event dan Listener untuk notifications
5. Brief explanation dari setiap extraction
Gunakan Laravel 11 dan PHP 8.2+ syntax.
Output Refactored:
1. Refactored Controller
<?php
namespace App\\Http\\Controllers\\Api\\V1;
use App\\Http\\Controllers\\Controller;
use App\\Http\\Requests\\Transaction\\StoreTransactionRequest;
use App\\Http\\Resources\\TransactionOutResource;
use App\\Services\\TransactionService;
use App\\Exceptions\\InsufficientStockException;
use App\\Exceptions\\ProductNotAvailableException;
use Illuminate\\Http\\JsonResponse;
use Symfony\\Component\\HttpFoundation\\Response;
class TransactionController extends Controller
{
public function __construct(
protected TransactionService $transactionService
) {}
public function store(StoreTransactionRequest $request): JsonResponse
{
try {
$transaction = $this->transactionService->createSaleTransaction(
data: $request->validated(),
userId: $request->user()->id
);
return $this->successResponse(
message: 'Transaction created successfully',
data: new TransactionOutResource($transaction),
status: Response::HTTP_CREATED
);
} catch (InsufficientStockException|ProductNotAvailableException $e) {
return $this->errorResponse($e->getMessage(), Response::HTTP_UNPROCESSABLE_ENTITY);
}
}
}
Dari 150+ baris jadi ~25 baris. Controller sekarang cuma orchestrate — validation di Form Request, logic di Service.
2. Form Request
<?php
namespace App\\Http\\Requests\\Transaction;
use Illuminate\\Foundation\\Http\\FormRequest;
use Illuminate\\Validation\\Rule;
class StoreTransactionRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'customer_name' => ['nullable', 'string', 'max:255'],
'customer_email' => ['nullable', 'email', 'max:255'],
'items' => ['required', 'array', 'min:1'],
'items.*.product_id' => [
'required',
'integer',
Rule::exists('products', 'id')->whereNull('deleted_at'),
],
'items.*.quantity' => ['required', 'integer', 'min:1'],
'discount' => ['nullable', 'numeric', 'min:0'],
'notes' => ['nullable', 'string', 'max:1000'],
];
}
public function messages(): array
{
return [
'items.required' => 'Minimal harus ada 1 item.',
'items.*.product_id.required' => 'Product harus dipilih.',
'items.*.product_id.exists' => 'Product tidak ditemukan.',
'items.*.quantity.min' => 'Quantity minimal 1.',
];
}
}
3. Service Class
<?php
namespace App\\Services;
use App\\Models\\Product;
use App\\Models\\TransactionOut;
use App\\Models\\TransactionOutItem;
use App\\Events\\TransactionCreated;
use App\\Exceptions\\InsufficientStockException;
use App\\Exceptions\\ProductNotAvailableException;
use App\\Traits\\GeneratesInvoiceNumber;
use Illuminate\\Support\\Collection;
use Illuminate\\Support\\Facades\\DB;
class TransactionService
{
use GeneratesInvoiceNumber;
/**
* Create a new sale transaction.
*/
public function createSaleTransaction(array $data, int $userId): TransactionOut
{
// Prepare dan validate items
$preparedItems = $this->prepareAndValidateItems($data['items']);
// Calculate totals
$totals = $this->calculateTotals($preparedItems, $data['discount'] ?? 0);
return DB::transaction(function () use ($data, $userId, $preparedItems, $totals) {
// Lock products untuk prevent race condition
$this->lockAndRevalidateStock($preparedItems);
// Create transaction
$transaction = $this->createTransaction($data, $userId, $totals);
// Create items dan reduce stock
$this->createItemsAndReduceStock($transaction, $preparedItems);
// Load relationships
$transaction->load(['items.product', 'user']);
// Dispatch event (handles notifications, logging, etc.)
event(new TransactionCreated($transaction, $data));
return $transaction;
});
}
protected function prepareAndValidateItems(array $items): Collection
{
return collect($items)->map(function ($item) {
$product = Product::find($item['product_id']);
if (!$product) {
throw new ProductNotAvailableException("Product not found");
}
if (!$product->is_active) {
throw new ProductNotAvailableException("Product {$product->name} is not active");
}
if ($product->stock < $item['quantity']) {
throw new InsufficientStockException(
"Insufficient stock for {$product->name}. Available: {$product->stock}"
);
}
return [
'product' => $product,
'quantity' => $item['quantity'],
'price' => $product->sell_price,
'subtotal' => $product->sell_price * $item['quantity'],
];
});
}
protected function calculateTotals(Collection $items, float $discount): array
{
$subtotal = $items->sum('subtotal');
if ($discount > $subtotal) {
throw new \\InvalidArgumentException('Discount cannot exceed subtotal');
}
return [
'subtotal' => $subtotal,
'discount' => $discount,
'total' => $subtotal - $discount,
];
}
protected function lockAndRevalidateStock(Collection $items): void
{
$productIds = $items->pluck('product.id');
$lockedProducts = Product::whereIn('id', $productIds)
->lockForUpdate()
->get()
->keyBy('id');
foreach ($items as $item) {
$product = $lockedProducts->get($item['product']->id);
if ($product->stock < $item['quantity']) {
throw new InsufficientStockException(
"Stock changed. {$product->name} now has {$product->stock} available"
);
}
}
}
protected function createTransaction(array $data, int $userId, array $totals): TransactionOut
{
return TransactionOut::create([
'user_id' => $userId,
'invoice_number' => $this->generateInvoiceNumber('INV-OUT'),
'transaction_date' => now()->toDateString(),
'customer_name' => $data['customer_name'] ?? null,
'subtotal' => $totals['subtotal'],
'discount' => $totals['discount'],
'total_amount' => $totals['total'],
'notes' => $data['notes'] ?? null,
]);
}
protected function createItemsAndReduceStock(TransactionOut $transaction, Collection $items): void
{
foreach ($items as $item) {
TransactionOutItem::create([
'transaction_out_id' => $transaction->id,
'product_id' => $item['product']->id,
'quantity' => $item['quantity'],
'price' => $item['price'],
'subtotal' => $item['subtotal'],
]);
$item['product']->decrement('stock', $item['quantity']);
}
}
}
4. Invoice Number Trait
<?php
namespace App\\Traits;
use Illuminate\\Support\\Facades\\DB;
trait GeneratesInvoiceNumber
{
protected function generateInvoiceNumber(string $prefix): string
{
$date = now()->format('Ymd');
$fullPrefix = "{$prefix}-{$date}-";
$lastNumber = DB::table('transactions_out')
->where('invoice_number', 'like', "{$fullPrefix}%")
->lockForUpdate()
->max(DB::raw("CAST(SUBSTRING(invoice_number, -4) AS UNSIGNED)"));
$newNumber = ($lastNumber ?? 0) + 1;
return $fullPrefix . str_pad($newNumber, 4, '0', STR_PAD_LEFT);
}
}
5. Event dan Listeners
TransactionCreated Event:
<?php
namespace App\\Events;
use App\\Models\\TransactionOut;
use Illuminate\\Foundation\\Events\\Dispatchable;
use Illuminate\\Queue\\SerializesModels;
class TransactionCreated
{
use Dispatchable, SerializesModels;
public function __construct(
public TransactionOut $transaction,
public array $requestData = []
) {}
}
SendTransactionReceipt Listener:
<?php
namespace App\\Listeners;
use App\\Events\\TransactionCreated;
use App\\Mail\\TransactionReceipt;
use Illuminate\\Contracts\\Queue\\ShouldQueue;
use Illuminate\\Support\\Facades\\Mail;
use Illuminate\\Support\\Facades\\Log;
class SendTransactionReceipt implements ShouldQueue
{
public function handle(TransactionCreated $event): void
{
$email = $event->requestData['customer_email'] ?? null;
if (!$email) {
return;
}
try {
Mail::to($email)->send(new TransactionReceipt($event->transaction));
} catch (\\Exception $e) {
Log::warning('Failed to send receipt email', [
'transaction_id' => $event->transaction->id,
'error' => $e->getMessage(),
]);
}
}
}
LogTransactionActivity Listener:
<?php
namespace App\\Listeners;
use App\\Events\\TransactionCreated;
use App\\Models\\ActivityLog;
class LogTransactionActivity
{
public function handle(TransactionCreated $event): void
{
ActivityLog::create([
'user_id' => $event->transaction->user_id,
'action' => 'created',
'model_type' => get_class($event->transaction),
'model_id' => $event->transaction->id,
'new_values' => $event->transaction->toArray(),
]);
}
}
CheckLowStockAlert Listener:
<?php
namespace App\\Listeners;
use App\\Events\\TransactionCreated;
use App\\Notifications\\LowStockAlert;
use App\\Models\\User;
use Illuminate\\Support\\Facades\\Notification;
class CheckLowStockAlert
{
public function handle(TransactionCreated $event): void
{
$lowStockProducts = $event->transaction->items
->map(fn ($item) => $item->product)
->filter(fn ($product) => $product->stock <= $product->min_stock);
if ($lowStockProducts->isEmpty()) {
return;
}
// Notify admins
$admins = User::where('role_id', 1)->get(); // Assuming role_id 1 is admin
Notification::send($admins, new LowStockAlert($lowStockProducts));
}
}
Register di EventServiceProvider:
protected $listen = [
TransactionCreated::class => [
LogTransactionActivity::class,
SendTransactionReceipt::class,
CheckLowStockAlert::class,
],
];
Case 2: Fix N+1 Query Problem
N+1 adalah silent performance killer. Sering nggak disadari sampai data membesar.
Prompt:
Analyze dan fix N+1 query problem di code berikut:
```php
public function getTransactionReport(Request $request)
{
$transactions = TransactionOut::whereBetween('transaction_date', [
$request->start_date,
$request->end_date,
])->get();
$report = [];
foreach ($transactions as $transaction) {
$items = [];
foreach ($transaction->items as $item) {
$items[] = [
'product_name' => $item->product->name,
'category' => $item->product->category->name,
'quantity' => $item->quantity,
'price' => $item->price,
'subtotal' => $item->subtotal,
];
}
$report[] = [
'invoice' => $transaction->invoice_number,
'date' => $transaction->transaction_date,
'cashier' => $transaction->user->name,
'total' => $transaction->total_amount,
'items' => $items,
];
}
return response()->json(['data' => $report]);
}
Identify:
- Berapa queries yang dijalankan untuk 100 transactions dengan masing-masing 5 items?
- Dimana N+1 terjadi?
- Bagaimana fix-nya?
Provide optimized version dengan eager loading yang tepat.
**Output:**
---
### Analysis
**Query count untuk 100 transactions × 5 items:**
1 query: Get transactions
- 100 queries: Get items untuk setiap transaction (N+1 #1)
- 500 queries: Get product untuk setiap item (N+1 #2)
- 500 queries: Get category untuk setiap product (N+1 #3)
- 100 queries: Get user untuk setiap transaction (N+1 #4) ───────────────────────────────── = 1,201 queries! 🔥
Untuk 100 transactions aja udah 1,200+ queries. Bayangkan kalau 1,000 transactions.
**N+1 locations:**
1. `$transaction->items` — items nggak di-eager load
2. `$item->product` — product nggak di-eager load
3. `$item->product->category` — category nggak di-eager load (nested)
4. `$transaction->user` — user nggak di-eager load
---
### Optimized Version
```php
public function getTransactionReport(Request $request)
{
$transactions = TransactionOut::query()
->with([
'user:id,name', // Select only needed columns
'items:id,transaction_out_id,product_id,quantity,price,subtotal',
'items.product:id,name,category_id', // Nested eager load
'items.product.category:id,name', // Nested nested eager load
])
->whereBetween('transaction_date', [
$request->start_date,
$request->end_date,
])
->select(['id', 'invoice_number', 'transaction_date', 'user_id', 'total_amount'])
->get();
// Sekarang semua data sudah loaded, no additional queries
$report = $transactions->map(function ($transaction) {
return [
'invoice' => $transaction->invoice_number,
'date' => $transaction->transaction_date,
'cashier' => $transaction->user->name,
'total' => $transaction->total_amount,
'items' => $transaction->items->map(fn ($item) => [
'product_name' => $item->product->name,
'category' => $item->product->category->name,
'quantity' => $item->quantity,
'price' => $item->price,
'subtotal' => $item->subtotal,
]),
];
});
return response()->json(['data' => $report]);
}
Query count setelah optimization:
1 query: Get transactions
1 query: Get all related users
1 query: Get all related items
1 query: Get all related products
1 query: Get all related categories
─────────────────────────────────
= 5 queries total ✅
From 1,201 queries to 5 queries — itu ~240x improvement.
Case 3: Apply SOLID Principles
Prompt:
Review class berikut dan refactor untuk better comply dengan SOLID principles:
```php
class OrderProcessor
{
public function process($order)
{
// Validate order
if (empty($order['items'])) {
throw new \\Exception('Order must have items');
}
// Calculate total
$total = 0;
foreach ($order['items'] as $item) {
$total += $item['price'] * $item['quantity'];
}
// Apply discount
if ($order['discount_code'] === 'SAVE10') {
$total = $total * 0.9;
} elseif ($order['discount_code'] === 'SAVE20') {
$total = $total * 0.8;
}
// Process payment
if ($order['payment_method'] === 'credit_card') {
$gateway = new \\Stripe\\PaymentGateway();
$gateway->charge($order['card_number'], $total);
} elseif ($order['payment_method'] === 'bank_transfer') {
$this->sendBankTransferInstructions($order['email'], $total);
}
// Save to database
DB::table('orders')->insert([
'customer_email' => $order['email'],
'total' => $total,
'status' => 'paid',
]);
// Send notification
Mail::to($order['email'])->send(new OrderConfirmation($order));
return $total;
}
}
Identify violations dan refactor dengan:
- Single Responsibility Principle
- Open/Closed Principle (especially untuk payment methods dan discounts)
- Dependency Inversion Principle
Provide refactored classes dengan penjelasan.
**Output Refactored:**
---
### SOLID Violations Identified
1. **SRP Violation**: Class handles validation, calculation, payment, persistence, dan notification
2. **OCP Violation**: Adding new payment method atau discount memerlukan modify class
3. **DIP Violation**: Directly instantiating Stripe gateway (tight coupling)
---
### Refactored Solution
**1. Order Validator (Single Responsibility)**
```php
<?php
namespace App\\Services\\Order;
use App\\Exceptions\\ValidationException;
class OrderValidator
{
public function validate(array $order): void
{
if (empty($order['items'])) {
throw new ValidationException('Order must have items');
}
if (empty($order['email'])) {
throw new ValidationException('Customer email is required');
}
// Add more validations as needed
}
}
2. Order Calculator (Single Responsibility)
<?php
namespace App\\Services\\Order;
class OrderCalculator
{
public function __construct(
protected DiscountStrategyInterface $discountStrategy
) {}
public function calculateTotal(array $items): float
{
return collect($items)->sum(fn ($item) => $item['price'] * $item['quantity']);
}
public function applyDiscount(float $total, ?string $discountCode): float
{
if (!$discountCode) {
return $total;
}
return $this->discountStrategy->apply($total, $discountCode);
}
}
3. Discount Strategy (Open/Closed Principle)
<?php
namespace App\\Contracts;
interface DiscountStrategyInterface
{
public function apply(float $total, string $code): float;
}
<?php
namespace App\\Services\\Order\\Discounts;
use App\\Contracts\\DiscountStrategyInterface;
class PercentageDiscountStrategy implements DiscountStrategyInterface
{
protected array $discounts = [
'SAVE10' => 0.10,
'SAVE20' => 0.20,
'SAVE50' => 0.50,
];
public function apply(float $total, string $code): float
{
$percentage = $this->discounts[$code] ?? 0;
return $total * (1 - $percentage);
}
public function addDiscount(string $code, float $percentage): void
{
$this->discounts[$code] = $percentage;
}
}
4. Payment Gateway Interface (Open/Closed + Dependency Inversion)
<?php
namespace App\\Contracts;
interface PaymentGatewayInterface
{
public function charge(array $paymentData, float $amount): PaymentResult;
}
<?php
namespace App\\Services\\Payment;
use App\\Contracts\\PaymentGatewayInterface;
class StripeGateway implements PaymentGatewayInterface
{
public function charge(array $paymentData, float $amount): PaymentResult
{
// Stripe implementation
$stripe = new \\Stripe\\StripeClient(config('services.stripe.secret'));
$charge = $stripe->charges->create([
'amount' => $amount * 100,
'currency' => 'idr',
'source' => $paymentData['card_token'],
]);
return new PaymentResult(
success: $charge->status === 'succeeded',
transactionId: $charge->id
);
}
}
<?php
namespace App\\Services\\Payment;
use App\\Contracts\\PaymentGatewayInterface;
class BankTransferGateway implements PaymentGatewayInterface
{
public function charge(array $paymentData, float $amount): PaymentResult
{
// Generate VA number, send instructions, etc.
$vaNumber = $this->generateVirtualAccount();
return new PaymentResult(
success: true,
transactionId: $vaNumber,
requiresAction: true,
actionType: 'bank_transfer'
);
}
}
5. Payment Gateway Factory
<?php
namespace App\\Services\\Payment;
use App\\Contracts\\PaymentGatewayInterface;
use InvalidArgumentException;
class PaymentGatewayFactory
{
protected array $gateways = [];
public function __construct()
{
$this->gateways = [
'credit_card' => StripeGateway::class,
'bank_transfer' => BankTransferGateway::class,
];
}
public function make(string $method): PaymentGatewayInterface
{
if (!isset($this->gateways[$method])) {
throw new InvalidArgumentException("Payment method {$method} not supported");
}
return app($this->gateways[$method]);
}
public function register(string $method, string $gatewayClass): void
{
$this->gateways[$method] = $gatewayClass;
}
}
6. Refactored Order Processor
<?php
namespace App\\Services\\Order;
use App\\Contracts\\PaymentGatewayInterface;
use App\\Events\\OrderCreated;
use App\\Models\\Order;
class OrderProcessor
{
public function __construct(
protected OrderValidator $validator,
protected OrderCalculator $calculator,
protected PaymentGatewayFactory $paymentFactory,
protected OrderRepository $repository
) {}
public function process(array $orderData): Order
{
// Validate
$this->validator->validate($orderData);
// Calculate
$subtotal = $this->calculator->calculateTotal($orderData['items']);
$total = $this->calculator->applyDiscount($subtotal, $orderData['discount_code'] ?? null);
// Process payment
$gateway = $this->paymentFactory->make($orderData['payment_method']);
$paymentResult = $gateway->charge($orderData, $total);
if (!$paymentResult->success) {
throw new PaymentFailedException($paymentResult->message);
}
// Persist
$order = $this->repository->create([
'customer_email' => $orderData['email'],
'subtotal' => $subtotal,
'discount' => $subtotal - $total,
'total' => $total,
'payment_transaction_id' => $paymentResult->transactionId,
'status' => $paymentResult->requiresAction ? 'pending' : 'paid',
]);
// Dispatch event (notification handled by listener)
event(new OrderCreated($order));
return $order;
}
}
Sekarang:
- SRP: Setiap class punya satu responsibility
- OCP: Tambah payment method atau discount = tambah class baru, nggak modify existing
- DIP: Depend on interfaces, bukan concrete implementations
Tips Refactoring dengan AI
Paste code yang problematic
AI butuh lihat actual code untuk kasih suggestions yang relevant.
Specify what bothers you
"Code ini works tapi feels messy" → kurang specific "Controller ini 200+ baris, ada logic yang harusnya di service" → lebih actionable
Ask for explanation
Sebelum refactor, jelaskan dulu:
1. Apa saja code smells yang kamu identify?
2. Principle apa yang dilanggar?
3. Approach refactoring yang akan diambil?
Iterative refactoring
Jangan refactor semuanya sekaligus. Step by step:
- Extract validation dulu
- Test
- Extract business logic
- Test
- Dan seterusnya
Checklist Refactoring
- [ ] Code smells identified
- [ ] Refactoring approach planned
- [ ] Tests exist sebelum refactor (safety net)
- [ ] Refactor incrementally dengan test di setiap step
- [ ] No functionality changed (behavior sama, structure better)
- [ ] Performance sama atau better
Clean code = maintainable code = happy future-you (dan happy client). Next, kita bahas gimana AI bantu write tests untuk ensure code yang udah di-refactor tetap works.
Bagian 8: Membuat Unit dan Feature Testing
"Tests? Ah nanti aja, deadline mepet."
Familiar? Saya juga dulu gitu. Sampai suatu hari, simple "fix bug" ternyata break 3 fitur lain yang nggak related. Client marah. Weekend hilang buat debugging.
Testing itu bukan luxury — ini insurance.
Dan kabar baiknya, nulis tests adalah area dimana AI sangat membantu. Karena tests itu pattern-nya repetitive, AI bisa generate bulk test cases dengan cepat.
Kenapa Testing Penting untuk Freelancer
Confidence saat deliver "Apakah code ini beneran works?" — dengan tests, kalian punya jawaban yang concrete.
Safety net untuk changes Mau refactor? Mau add feature? Run tests dulu. Kalau hijau semua, aman.
Reduce bug reports Bugs yang ketangkep di tests = bugs yang nggak sampai ke client.
Professional impression Client (yang tech-savvy) akan appreciate codebase yang ada tests-nya. Bisa jadi selling point.
Documentation yang executable Tests menjelaskan "gimana seharusnya sistem behave" dalam bentuk code yang bisa dijalankan.
Types of Tests di Laravel
Unit Tests
- Test individual class/method in isolation
- Fast, nggak perlu database atau HTTP
- Mock dependencies
- Location:
tests/Unit/
Feature Tests
- Test full feature/flow dari HTTP request sampai response
- Bisa involve database, auth, etc.
- Location:
tests/Feature/
Integration Tests
- Test interaction antara multiple components
- Biasanya include database
Untuk typical Laravel app, fokus utama biasanya di Feature Tests karena memberikan confidence tertinggi bahwa fitur works end-to-end.
Gimana AI Membantu Testing
AI bisa:
- Generate test cases dari code yang existing
- Suggest edge cases yang mungkin kelewatan
- Generate test data / factories
- Create mocks untuk dependencies
- Explain testing patterns
Case 1: Generate Feature Tests untuk API Endpoint
Kita mulai dari endpoint yang paling penting: Create Transaction.
Prompt:
Buatkan comprehensive Feature Tests untuk TransactionOut API endpoint di Laravel 11.
Endpoint: POST /api/v1/transactions
Controller: TransactionController@store
Service: TransactionService
Request body:
{
"items": [
{ "product_id": 1, "quantity": 2 },
{ "product_id": 2, "quantity": 1 }
],
"discount": 10000,
"notes": "Test transaction"
}
Business rules yang harus di-test:
1. User harus authenticated
2. Items harus ada minimal 1
3. Product harus exist dan active
4. Stock harus mencukupi
5. Discount tidak boleh lebih dari subtotal
6. Setelah success: stock berkurang, transaction record created, items created
7. Invoice number auto-generated dengan format INV-OUT-YYYYMMDD-XXXX
Generate:
1. Test cases list (happy path dan edge cases)
2. Feature test class lengkap
3. Factory yang dibutuhkan
4. Penjelasan setiap test method
Gunakan:
- PHPUnit dengan Laravel assertions
- Database transactions (RefreshDatabase)
- Factories untuk test data
- Proper naming convention (test_xxx_when_yyy)
Output:
Test Cases List
Happy Path:
- ✅ Authenticated user can create transaction with valid data
- ✅ Transaction creates correct number of items
- ✅ Stock is reduced after successful transaction
- ✅ Invoice number is auto-generated with correct format
- ✅ Discount is applied correctly to total
- ✅ Response contains all expected fields
Authentication & Authorization: 7. ❌ Unauthenticated user cannot create transaction (401) 8. ❌ User without permission cannot create transaction (403) — if applicable
Validation - Items: 9. ❌ Cannot create transaction without items (422) 10. ❌ Cannot create transaction with empty items array (422) 11. ❌ Cannot create transaction with non-existent product (422) 12. ❌ Cannot create transaction with inactive product (422) 13. ❌ Cannot create transaction with quantity less than 1 (422) 14. ❌ Cannot create transaction with duplicate product IDs (422)
Validation - Stock: 15. ❌ Cannot create transaction when stock insufficient (422) 16. ❌ Stock validation message shows available stock
Validation - Discount: 17. ❌ Cannot create transaction with negative discount (422) 18. ❌ Cannot create transaction with discount exceeding subtotal (422) 19. ✅ Transaction without discount works correctly
Edge Cases: 20. ✅ Transaction with multiple items from same category 21. ✅ Transaction with maximum allowed items 22. ✅ Transaction with zero discount 23. ✅ Invoice number increments correctly for same day 24. ⚠️ Concurrent transactions don't get duplicate invoice numbers
Data Integrity: 25. ✅ Product price snapshot is stored (not reference) 26. ✅ Item subtotal is calculated correctly 27. ✅ Transaction total equals subtotal minus discount
TransactionFeatureTest.php
<?php
namespace Tests\\Feature\\Api\\V1;
use App\\Models\\User;
use App\\Models\\Role;
use App\\Models\\Product;
use App\\Models\\Category;
use App\\Models\\TransactionOut;
use Illuminate\\Foundation\\Testing\\RefreshDatabase;
use Tests\\TestCase;
class TransactionFeatureTest extends TestCase
{
use RefreshDatabase;
protected User $user;
protected Category $category;
protected function setUp(): void
{
parent::setUp();
// Create role and user
$role = Role::factory()->create(['name' => 'staff']);
$this->user = User::factory()->create(['role_id' => $role->id]);
// Create category for products
$this->category = Category::factory()->create();
}
// ==================== HAPPY PATH TESTS ====================
/** @test */
public function authenticated_user_can_create_transaction_with_valid_data(): void
{
// Arrange
$products = Product::factory()
->count(2)
->create([
'category_id' => $this->category->id,
'stock' => 100,
'sell_price' => 50000,
]);
$payload = [
'items' => [
['product_id' => $products[0]->id, 'quantity' => 2],
['product_id' => $products[1]->id, 'quantity' => 3],
],
'discount' => 10000,
'notes' => 'Test transaction',
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(201)
->assertJson([
'success' => true,
'message' => 'Transaction created successfully',
])
->assertJsonStructure([
'data' => [
'id',
'invoice_number',
'transaction_date',
'subtotal',
'discount',
'total_amount',
'items',
],
]);
// Verify database
$this->assertDatabaseHas('transactions_out', [
'user_id' => $this->user->id,
'subtotal' => 250000, // (2 * 50000) + (3 * 50000)
'discount' => 10000,
'total_amount' => 240000,
]);
}
/** @test */
public function transaction_creates_correct_number_of_items(): void
{
// Arrange
$products = Product::factory()->count(3)->create([
'category_id' => $this->category->id,
'stock' => 50,
]);
$payload = [
'items' => $products->map(fn ($p) => [
'product_id' => $p->id,
'quantity' => 1,
])->toArray(),
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(201);
$transactionId = $response->json('data.id');
$this->assertDatabaseCount('transaction_out_items', 3);
foreach ($products as $product) {
$this->assertDatabaseHas('transaction_out_items', [
'transaction_out_id' => $transactionId,
'product_id' => $product->id,
'quantity' => 1,
]);
}
}
/** @test */
public function stock_is_reduced_after_successful_transaction(): void
{
// Arrange
$product = Product::factory()->create([
'category_id' => $this->category->id,
'stock' => 100,
'sell_price' => 25000,
]);
$payload = [
'items' => [
['product_id' => $product->id, 'quantity' => 15],
],
];
// Act
$this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload)
->assertStatus(201);
// Assert
$this->assertEquals(85, $product->fresh()->stock);
}
/** @test */
public function invoice_number_is_generated_with_correct_format(): void
{
// Arrange
$product = Product::factory()->create([
'category_id' => $this->category->id,
'stock' => 10,
]);
$payload = [
'items' => [
['product_id' => $product->id, 'quantity' => 1],
],
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$invoiceNumber = $response->json('data.invoice_number');
$expectedPrefix = 'INV-OUT-' . now()->format('Ymd') . '-';
$this->assertStringStartsWith($expectedPrefix, $invoiceNumber);
$this->assertMatchesRegularExpression('/^INV-OUT-\\d{8}-\\d{4}$/', $invoiceNumber);
}
/** @test */
public function invoice_number_increments_for_same_day_transactions(): void
{
// Arrange
$product = Product::factory()->create([
'category_id' => $this->category->id,
'stock' => 100,
]);
$payload = [
'items' => [
['product_id' => $product->id, 'quantity' => 1],
],
];
// Act - Create 3 transactions
$invoiceNumbers = [];
for ($i = 0; $i < 3; $i++) {
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
$response->assertStatus(201);
$invoiceNumbers[] = $response->json('data.invoice_number');
}
// Assert - Invoice numbers should be sequential
$this->assertStringEndsWith('-0001', $invoiceNumbers[0]);
$this->assertStringEndsWith('-0002', $invoiceNumbers[1]);
$this->assertStringEndsWith('-0003', $invoiceNumbers[2]);
}
/** @test */
public function product_price_is_snapshot_not_reference(): void
{
// Arrange
$originalPrice = 100000;
$product = Product::factory()->create([
'category_id' => $this->category->id,
'stock' => 10,
'sell_price' => $originalPrice,
]);
$payload = [
'items' => [
['product_id' => $product->id, 'quantity' => 2],
],
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Change product price after transaction
$product->update(['sell_price' => 150000]);
// Assert - Transaction item should have original price
$this->assertDatabaseHas('transaction_out_items', [
'transaction_out_id' => $response->json('data.id'),
'price' => $originalPrice,
'subtotal' => $originalPrice * 2,
]);
}
// ==================== AUTHENTICATION TESTS ====================
/** @test */
public function unauthenticated_user_cannot_create_transaction(): void
{
// Arrange
$product = Product::factory()->create([
'category_id' => $this->category->id,
]);
$payload = [
'items' => [
['product_id' => $product->id, 'quantity' => 1],
],
];
// Act
$response = $this->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(401)
->assertJson([
'message' => 'Unauthenticated.',
]);
}
// ==================== VALIDATION TESTS - ITEMS ====================
/** @test */
public function cannot_create_transaction_without_items(): void
{
// Arrange
$payload = [
'discount' => 0,
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(422)
->assertJsonValidationErrors(['items']);
}
/** @test */
public function cannot_create_transaction_with_empty_items_array(): void
{
// Arrange
$payload = [
'items' => [],
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(422)
->assertJsonValidationErrors(['items']);
}
/** @test */
public function cannot_create_transaction_with_nonexistent_product(): void
{
// Arrange
$payload = [
'items' => [
['product_id' => 99999, 'quantity' => 1],
],
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(422)
->assertJsonValidationErrors(['items.0.product_id']);
}
/** @test */
public function cannot_create_transaction_with_inactive_product(): void
{
// Arrange
$product = Product::factory()->create([
'category_id' => $this->category->id,
'is_active' => false,
'stock' => 100,
]);
$payload = [
'items' => [
['product_id' => $product->id, 'quantity' => 1],
],
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(422);
$this->assertStringContainsString(
'not active',
$response->json('message') ?? $response->json('errors.product.0')
);
}
/** @test */
public function cannot_create_transaction_with_quantity_less_than_one(): void
{
// Arrange
$product = Product::factory()->create([
'category_id' => $this->category->id,
]);
$payload = [
'items' => [
['product_id' => $product->id, 'quantity' => 0],
],
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(422)
->assertJsonValidationErrors(['items.0.quantity']);
}
// ==================== VALIDATION TESTS - STOCK ====================
/** @test */
public function cannot_create_transaction_when_stock_insufficient(): void
{
// Arrange
$product = Product::factory()->create([
'category_id' => $this->category->id,
'stock' => 5,
]);
$payload = [
'items' => [
['product_id' => $product->id, 'quantity' => 10],
],
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(422);
$this->assertStringContainsString(
'Insufficient stock',
$response->json('message')
);
}
/** @test */
public function stock_error_message_shows_available_quantity(): void
{
// Arrange
$product = Product::factory()->create([
'category_id' => $this->category->id,
'stock' => 7,
'name' => 'Test Product',
]);
$payload = [
'items' => [
['product_id' => $product->id, 'quantity' => 20],
],
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(422);
$message = $response->json('message');
$this->assertStringContainsString('7', $message); // Available stock
$this->assertStringContainsString('Test Product', $message);
}
// ==================== VALIDATION TESTS - DISCOUNT ====================
/** @test */
public function cannot_create_transaction_with_negative_discount(): void
{
// Arrange
$product = Product::factory()->create([
'category_id' => $this->category->id,
'stock' => 10,
]);
$payload = [
'items' => [
['product_id' => $product->id, 'quantity' => 1],
],
'discount' => -5000,
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(422)
->assertJsonValidationErrors(['discount']);
}
/** @test */
public function cannot_create_transaction_with_discount_exceeding_subtotal(): void
{
// Arrange
$product = Product::factory()->create([
'category_id' => $this->category->id,
'stock' => 10,
'sell_price' => 50000,
]);
$payload = [
'items' => [
['product_id' => $product->id, 'quantity' => 1], // Subtotal: 50000
],
'discount' => 100000, // More than subtotal
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(422);
$this->assertStringContainsString(
'Discount cannot exceed',
$response->json('message') ?? ''
);
}
/** @test */
public function transaction_without_discount_works_correctly(): void
{
// Arrange
$product = Product::factory()->create([
'category_id' => $this->category->id,
'stock' => 10,
'sell_price' => 75000,
]);
$payload = [
'items' => [
['product_id' => $product->id, 'quantity' => 2],
],
// No discount field
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(201);
$this->assertEquals(150000, $response->json('data.subtotal'));
$this->assertEquals(0, $response->json('data.discount'));
$this->assertEquals(150000, $response->json('data.total_amount'));
}
// ==================== DATA INTEGRITY TESTS ====================
/** @test */
public function item_subtotal_is_calculated_correctly(): void
{
// Arrange
$product = Product::factory()->create([
'category_id' => $this->category->id,
'stock' => 100,
'sell_price' => 35000,
]);
$quantity = 7;
$expectedSubtotal = 35000 * 7; // 245000
$payload = [
'items' => [
['product_id' => $product->id, 'quantity' => $quantity],
],
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(201);
$this->assertDatabaseHas('transaction_out_items', [
'transaction_out_id' => $response->json('data.id'),
'product_id' => $product->id,
'quantity' => $quantity,
'price' => 35000,
'subtotal' => $expectedSubtotal,
]);
}
/** @test */
public function transaction_total_equals_subtotal_minus_discount(): void
{
// Arrange
$products = Product::factory()->count(2)->create([
'category_id' => $this->category->id,
'stock' => 50,
'sell_price' => 100000,
]);
$discount = 25000;
$expectedSubtotal = 100000 * 2 + 100000 * 3; // 500000
$expectedTotal = $expectedSubtotal - $discount; // 475000
$payload = [
'items' => [
['product_id' => $products[0]->id, 'quantity' => 2],
['product_id' => $products[1]->id, 'quantity' => 3],
],
'discount' => $discount,
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(201);
$this->assertEquals($expectedSubtotal, $response->json('data.subtotal'));
$this->assertEquals($discount, $response->json('data.discount'));
$this->assertEquals($expectedTotal, $response->json('data.total_amount'));
}
// ==================== EDGE CASE TESTS ====================
/** @test */
public function can_create_transaction_with_single_item(): void
{
// Arrange
$product = Product::factory()->create([
'category_id' => $this->category->id,
'stock' => 5,
]);
$payload = [
'items' => [
['product_id' => $product->id, 'quantity' => 1],
],
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(201);
$this->assertCount(1, $response->json('data.items'));
}
/** @test */
public function stock_exactly_equals_quantity_works(): void
{
// Arrange
$product = Product::factory()->create([
'category_id' => $this->category->id,
'stock' => 5, // Exactly 5
]);
$payload = [
'items' => [
['product_id' => $product->id, 'quantity' => 5], // Order all 5
],
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(201);
$this->assertEquals(0, $product->fresh()->stock);
}
/** @test */
public function failed_transaction_does_not_reduce_stock(): void
{
// Arrange
$product1 = Product::factory()->create([
'category_id' => $this->category->id,
'stock' => 100,
'sell_price' => 10000,
]);
$product2 = Product::factory()->create([
'category_id' => $this->category->id,
'stock' => 2, // Low stock
'sell_price' => 10000,
]);
$payload = [
'items' => [
['product_id' => $product1->id, 'quantity' => 5],
['product_id' => $product2->id, 'quantity' => 10], // Will fail - not enough stock
],
];
// Act
$response = $this->actingAs($this->user)
->postJson('/api/v1/transactions', $payload);
// Assert
$response->assertStatus(422);
// Both products should have unchanged stock
$this->assertEquals(100, $product1->fresh()->stock);
$this->assertEquals(2, $product2->fresh()->stock);
// No transaction should be created
$this->assertDatabaseCount('transactions_out', 0);
}
}
Factories yang Dibutuhkan
RoleFactory.php:
<?php
namespace Database\\Factories;
use Illuminate\\Database\\Eloquent\\Factories\\Factory;
class RoleFactory extends Factory
{
public function definition(): array
{
return [
'name' => fake()->unique()->randomElement(['admin', 'staff', 'viewer']),
'display_name' => fn (array $attrs) => ucfirst($attrs['name']),
];
}
public function admin(): self
{
return $this->state(['name' => 'admin', 'display_name' => 'Admin']);
}
public function staff(): self
{
return $this->state(['name' => 'staff', 'display_name' => 'Staff']);
}
}
CategoryFactory.php:
<?php
namespace Database\\Factories;
use Illuminate\\Database\\Eloquent\\Factories\\Factory;
use Illuminate\\Support\\Str;
class CategoryFactory extends Factory
{
public function definition(): array
{
$name = fake()->unique()->words(2, true);
return [
'name' => ucfirst($name),
'slug' => Str::slug($name),
'description' => fake()->optional()->sentence(),
];
}
}
ProductFactory.php:
<?php
namespace Database\\Factories;
use App\\Models\\Category;
use Illuminate\\Database\\Eloquent\\Factories\\Factory;
class ProductFactory extends Factory
{
public function definition(): array
{
$buyPrice = fake()->numberBetween(10000, 500000);
return [
'category_id' => Category::factory(),
'name' => fake()->words(3, true),
'sku' => 'PRD-' . strtoupper(fake()->unique()->bothify('????-####')),
'description' => fake()->optional()->paragraph(),
'image' => null,
'buy_price' => $buyPrice,
'sell_price' => $buyPrice * fake()->randomFloat(2, 1.1, 1.5), // 10-50% markup
'stock' => fake()->numberBetween(0, 100),
'min_stock' => fake()->numberBetween(5, 20),
'is_active' => true,
];
}
public function inactive(): self
{
return $this->state(['is_active' => false]);
}
public function outOfStock(): self
{
return $this->state(['stock' => 0]);
}
public function lowStock(): self
{
return $this->state(fn () => [
'stock' => fake()->numberBetween(1, 5),
'min_stock' => 10,
]);
}
public function withStock(int $quantity): self
{
return $this->state(['stock' => $quantity]);
}
public function withPrice(float $buyPrice, float $sellPrice): self
{
return $this->state([
'buy_price' => $buyPrice,
'sell_price' => $sellPrice,
]);
}
}
Case 2: Generate Unit Tests untuk Service Class
Prompt:
Buatkan Unit Tests untuk ProfitCalculationService yang sudah dibuat di bagian sebelumnya.
Focus pada method calculateItemCOGS dengan FIFO logic:
- Test dengan single batch
- Test dengan multiple batches
- Test FIFO order (oldest first)
- Test when quantity exceeds single batch
- Test with no purchase history (fallback)
Gunakan Mocking untuk database queries dimana appropriate.
Output:
<?php
namespace Tests\\Unit\\Services;
use App\\Models\\Product;
use App\\Models\\TransactionInItem;
use App\\Models\\TransactionOutItem;
use App\\Services\\ProfitCalculationService;
use Illuminate\\Foundation\\Testing\\RefreshDatabase;
use Tests\\TestCase;
class ProfitCalculationServiceTest extends TestCase
{
use RefreshDatabase;
protected ProfitCalculationService $service;
protected function setUp(): void
{
parent::setUp();
$this->service = new ProfitCalculationService();
}
/** @test */
public function calculates_cogs_correctly_with_single_batch(): void
{
// Arrange
$product = Product::factory()->create(['buy_price' => 10000]);
// Simulate purchase: 100 units @ 10000
$this->createPurchase($product->id, 100, 10000, '2024-01-01');
// Act - Calculate COGS for 30 units
$cogs = $this->invokeCalculateItemCOGS($product->id, 30, '2024-01-15');
// Assert
$this->assertEquals(300000, $cogs); // 30 × 10000
}
/** @test */
public function calculates_cogs_using_fifo_with_multiple_batches(): void
{
// Arrange
$product = Product::factory()->create();
// Batch 1: 10 units @ 100 (Jan 1)
$this->createPurchase($product->id, 10, 100, '2024-01-01');
// Batch 2: 10 units @ 120 (Jan 5)
$this->createPurchase($product->id, 10, 120, '2024-01-05');
// Batch 3: 10 units @ 150 (Jan 10)
$this->createPurchase($product->id, 10, 150, '2024-01-10');
// Act - Calculate COGS for 15 units
// Should use: 10 from Batch 1 (100) + 5 from Batch 2 (120)
$cogs = $this->invokeCalculateItemCOGS($product->id, 15, '2024-01-15');
// Assert
$expectedCOGS = (10 * 100) + (5 * 120); // 1000 + 600 = 1600
$this->assertEquals($expectedCOGS, $cogs);
}
/** @test */
public function fifo_uses_oldest_batch_first(): void
{
// Arrange
$product = Product::factory()->create();
// Older batch with lower price
$this->createPurchase($product->id, 50, 80, '2024-01-01');
// Newer batch with higher price
$this->createPurchase($product->id, 50, 100, '2024-01-10');
// Act - Calculate for 30 units
$cogs = $this->invokeCalculateItemCOGS($product->id, 30, '2024-01-15');
// Assert - Should use older batch price (80), not newer (100)
$this->assertEquals(30 * 80, $cogs);
$this->assertNotEquals(30 * 100, $cogs);
}
/** @test */
public function handles_quantity_spanning_multiple_batches(): void
{
// Arrange
$product = Product::factory()->create();
$this->createPurchase($product->id, 5, 100, '2024-01-01');
$this->createPurchase($product->id, 5, 200, '2024-01-02');
$this->createPurchase($product->id, 5, 300, '2024-01-03');
// Act - Need 12 units (spans all 3 batches)
$cogs = $this->invokeCalculateItemCOGS($product->id, 12, '2024-01-15');
// Assert
// 5 × 100 + 5 × 200 + 2 × 300 = 500 + 1000 + 600 = 2100
$expectedCOGS = (5 * 100) + (5 * 200) + (2 * 300);
$this->assertEquals($expectedCOGS, $cogs);
}
/** @test */
public function uses_fallback_price_when_no_purchase_history(): void
{
// Arrange
$product = Product::factory()->create(['buy_price' => 50000]);
// No purchases created
// Act
$cogs = $this->invokeCalculateItemCOGS($product->id, 10, '2024-01-15');
// Assert - Should use product's buy_price as fallback
$this->assertEquals(10 * 50000, $cogs);
}
/** @test */
public function uses_fallback_for_excess_quantity(): void
{
// Arrange
$product = Product::factory()->create(['buy_price' => 75]);
// Only 10 units purchased
$this->createPurchase($product->id, 10, 100, '2024-01-01');
// Act - Request 15 units (5 more than purchased)
$cogs = $this->invokeCalculateItemCOGS($product->id, 15, '2024-01-15');
// Assert
// 10 × 100 (from batch) + 5 × 75 (fallback) = 1000 + 375 = 1375
$expectedCOGS = (10 * 100) + (5 * 75);
$this->assertEquals($expectedCOGS, $cogs);
}
/** @test */
public function only_considers_purchases_before_sale_date(): void
{
// Arrange
$product = Product::factory()->create(['buy_price' => 50]);
// Purchase before sale date
$this->createPurchase($product->id, 10, 100, '2024-01-01');
// Purchase after sale date (should be ignored)
$this->createPurchase($product->id, 10, 200, '2024-01-20');
// Act - Sale on Jan 15
$cogs = $this->invokeCalculateItemCOGS($product->id, 15, '2024-01-15');
// Assert
// 10 from batch @ 100 + 5 fallback @ 50 = 1000 + 250 = 1250
// NOT using the Jan 20 batch
$expectedCOGS = (10 * 100) + (5 * 50);
$this->assertEquals($expectedCOGS, $cogs);
}
// ==================== HELPER METHODS ====================
protected function createPurchase(int $productId, int $qty, float $price, string $date): void
{
$transaction = \\App\\Models\\TransactionIn::factory()->create([
'transaction_date' => $date,
]);
TransactionInItem::create([
'transaction_in_id' => $transaction->id,
'product_id' => $productId,
'quantity' => $qty,
'price' => $price,
'subtotal' => $qty * $price,
]);
}
protected function invokeCalculateItemCOGS(int $productId, int $quantity, string $saleDate): float
{
// Use reflection to access protected method
$reflection = new \\ReflectionClass($this->service);
$method = $reflection->getMethod('calculateItemCOGS');
$method->setAccessible(true);
return $method->invoke($this->service, $productId, $quantity, $saleDate);
}
}
Running Tests
# Run semua tests
php artisan test
# Run specific test file
php artisan test tests/Feature/Api/V1/TransactionFeatureTest.php
# Run dengan coverage report
php artisan test --coverage
# Run specific test method
php artisan test --filter=authenticated_user_can_create_transaction
# Run dengan verbose output
php artisan test -v
Tips Testing dengan AI
Minta test cases list dulu
Sebelum generate code, minta AI list dulu semua scenarios yang perlu di-test. Review, tambahin kalau ada yang kurang.
Specify edge cases explicitly
Tambahin test cases untuk:
- Concurrent transactions (race condition)
- Very large numbers (overflow?)
- Unicode characters di notes
- SQL injection attempt di search
Generate factories bersamaan
Tests butuh data. Minta AI generate factories dengan states yang useful.
Ask for test organization
Bagaimana organize test files untuk:
- 20 API endpoints
- 10 Service classes
- 5 complex features
Recommend folder structure dan naming conventions.
Checklist Testing
- [ ] Happy path tests untuk semua main features
- [ ] Validation tests untuk semua inputs
- [ ] Authentication/authorization tests
- [ ] Edge cases covered
- [ ] Database state verified (assertDatabaseHas, etc.)
- [ ] Error responses tested
- [ ] Tests actually run dan pass
- [ ] Coverage reasonable untuk critical paths
Tests = confidence. Dengan tests yang solid, kalian bisa deliver dengan yakin bahwa code works as expected. Next, kita bahas gimana AI bantu debug ketika things go wrong.
Bagian 9: Debugging dan Error Fixing
Production down. Error 500. Client panik. WhatsApp berbunyi terus.
Ini nightmare setiap developer. Dan di saat kayak gini, kemampuan debugging yang cepat dan systematic bisa jadi penyelamat.
Good news: AI bisa jadi partner debugging yang sangat helpful. Bukan cuma untuk explain error, tapi juga untuk brainstorm possible causes dan suggest fixes.
Common Debugging Scenarios
Sebagai freelancer Laravel, ini tipe errors yang paling sering ketemu:
- Query/Database Errors — SQL errors, constraint violations, N+1
- Logic Errors — Code jalan tapi hasil salah
- Performance Issues — Lambat, timeout, memory exhausted
- Integration Errors — API pihak ketiga fail, webhook nggak jalan
- Environment Issues — Works di local, fail di production
AI bisa bantu di semua scenarios ini.
Case 1: Debugging SQL Error
Error message yang cryptic bikin frustasi. AI bisa bantu decode dan explain.
Scenario:
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update
a child row: a foreign key constraint fails (`inventory`.`transaction_items`,
CONSTRAINT `transaction_items_product_id_foreign` FOREIGN KEY (`product_id`)
REFERENCES `products` (`id`))
Prompt:
Saya dapat error ini di Laravel:
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update
a child row: a foreign key constraint fails (`inventory`.`transaction_items`,
CONSTRAINT `transaction_items_product_id_foreign` FOREIGN KEY (`product_id`)
REFERENCES `products` (`id`))
Context:
- Saya mencoba create transaction dengan items
- Product dengan ID tersebut PASTI exists di database (sudah cek manual)
- Menggunakan soft deletes di products table
- Laravel 11, MySQL 8
Code yang trigger error:
```php
TransactionItem::create([
'transaction_id' => $transaction->id,
'product_id' => $productId, // Value: 5
'quantity' => 2,
'price' => 50000,
]);
Jelaskan:
- Apa sebenarnya penyebab error ini
- Kemungkinan root causes (list semua possibilities)
- Cara debug step by step
- Solusi yang recommended
**Output dari Claude:**
---
### Error Explanation
Error `1452 foreign key constraint fails` artinya MySQL mencoba insert row dengan foreign key value yang **tidak ada** di referenced table (products).
Tapi kalian bilang product exists. Ini berarti ada "disconnect" antara apa yang kalian lihat dan apa yang database lihat.
---
### Kemungkinan Root Causes
**1. Soft Delete Issue (Most Likely)**
Kalau `products` table pakai soft deletes, product dengan `deleted_at` IS NOT NULL dianggap "tidak ada" untuk foreign key constraint — tergantung bagaimana foreign key di-define.
Cek apakah product di-soft delete:
```sql
SELECT id, name, deleted_at FROM products WHERE id = 5;
2. Transaction Isolation
Product mungkin di-create dalam transaction yang belum committed. Kalau code kalian baca product di satu transaction tapi insert item di transaction lain, bisa terjadi race condition.
3. Different Database Connections
Laravel bisa punya multiple database connections. Mungkin kalian cek product di connection A, tapi insert ke connection B.
4. ID Mismatch
Variable $productId mungkin berisi value yang berbeda dari yang kalian expect. Bisa karena:
- Type juggling (string "5" vs integer 5 — usually fine tapi worth checking)
- Variable di-overwrite somewhere
- Loop issue
5. Data Cached Locally
Kalian mungkin cek via Tinker atau query builder yang sudah cache result, tapi actual row sudah deleted/changed.
6. Migration Issue
Foreign key constraint mungkin reference ke table/column yang salah.
Debug Steps
Step 1: Verify product exists (raw query)
// Di code, sebelum insert
$exists = DB::table('products')
->where('id', $productId)
->whereNull('deleted_at') // Include ini!
->exists();
Log::info("Product {$productId} exists: " . ($exists ? 'yes' : 'no'));
// Atau dump seluruh record
$product = DB::table('products')->where('id', $productId)->first();
dd($product);
Step 2: Check deleted_at specifically
$product = DB::table('products')
->where('id', $productId)
->first();
if ($product && $product->deleted_at !== null) {
Log::error("Product {$productId} is soft deleted!");
}
Step 3: Verify variable value
Log::info("Inserting item with product_id: {$productId}, type: " . gettype($productId));
Step 4: Check constraint definition
SHOW CREATE TABLE transaction_items;
Look at the FOREIGN KEY definition — does it match your expected table and column?
Step 5: Check if using different connections
// Log current connection
Log::info("Connection: " . DB::getDefaultConnection());
Solutions
Solution A: If Soft Delete is the Issue
Option 1 — Don't allow transaction with soft-deleted product:
$product = Product::findOrFail($productId); // This excludes soft-deleted by default
// Lanjut create transaction item
Option 2 — Include soft-deleted untuk lookup tapi validate:
$product = Product::withTrashed()->find($productId);
if (!$product) {
throw new ProductNotFoundException("Product not found");
}
if ($product->trashed()) {
throw new ProductNotAvailableException("Product has been deleted");
}
Option 3 — Modify foreign key to SET NULL on soft delete (kalau business logic allow):
// Di migration
$table->foreignId('product_id')->nullable()->constrained()->nullOnDelete();
Solution B: If Transaction Isolation Issue
Pastikan product creation dan item creation dalam satu DB transaction:
DB::transaction(function () use ($data) {
$product = Product::create([...]);
TransactionItem::create([
'product_id' => $product->id, // Now guaranteed to exist
// ...
]);
});
Solution C: General Defensive Coding
public function createTransactionItem(int $productId, array $data): TransactionItem
{
// Verify product exists dan active sebelum insert
$product = Product::where('id', $productId)
->where('is_active', true)
->firstOrFail(); // Throws 404 if not found
return TransactionItem::create([
'product_id' => $product->id, // Use model's ID, not input
// ...
]);
}
Case 2: Debugging Performance Issue
Slow queries adalah silent killer. AI bisa bantu identify bottlenecks.
Scenario:
Endpoint GET /api/transactions sangat lambat. 5-10 detik untuk 1000 records.
Prompt:
Endpoint ini sangat lambat (5-10 detik) untuk 1000 records:
```php
public function index(Request $request)
{
$transactions = TransactionOut::query()
->when($request->start_date, function ($q) use ($request) {
$q->where('transaction_date', '>=', $request->start_date);
})
->when($request->end_date, function ($q) use ($request) {
$q->where('transaction_date', '<=', $request->end_date);
})
->orderBy('transaction_date', 'desc')
->paginate(50);
// Transform for response
$data = $transactions->map(function ($transaction) {
$items = $transaction->items;
$totalQty = 0;
$productNames = [];
foreach ($items as $item) {
$totalQty += $item->quantity;
$productNames[] = $item->product->name;
}
return [
'id' => $transaction->id,
'invoice_number' => $transaction->invoice_number,
'date' => $transaction->transaction_date,
'total' => $transaction->total_amount,
'item_count' => $items->count(),
'total_qty' => $totalQty,
'products' => implode(', ', $productNames),
'cashier' => $transaction->user->name,
];
});
return response()->json([
'data' => $data,
'meta' => [...],
]);
}
Tables:
- transactions_out: ~5000 records
- transaction_out_items: ~25000 records
- products: ~500 records
Tolong:
- Identify semua performance issues
- Explain kenapa lambat
- Provide optimized version
- Estimate improvement yang expected
**Output:**
---
### Performance Issues Identified
**Issue 1: N+1 Query pada Items (CRITICAL)**
```php
$items = $transaction->items; // Query untuk setiap transaction
Untuk 50 transactions per page = 50 queries just for items.
Issue 2: N+1 Query pada Product (CRITICAL)
$item->product->name // Query untuk setiap item
Kalau average 5 items per transaction, itu 50 × 5 = 250 queries untuk products.
Issue 3: N+1 Query pada User (MODERATE)
$transaction->user->name // Query untuk setiap transaction
Another 50 queries.
Issue 4: No Index pada transaction_date (MODERATE)
->where('transaction_date', '>=', $request->start_date)
->orderBy('transaction_date', 'desc')
Filter dan sort by transaction_date tanpa index = full table scan.
Total Queries untuk 50 Records:
1 (transactions)
+ 50 (items per transaction)
+ 250 (products per item, assuming 5 items each)
+ 50 (users per transaction)
────────────────────────
= 351 queries! 😱
Multiply by network latency untuk setiap query, no wonder it's slow.
Optimized Version
public function index(Request $request)
{
$transactions = TransactionOut::query()
->select([
'id',
'invoice_number',
'transaction_date',
'total_amount',
'user_id'
])
// Eager load dengan specific columns
->with([
'user:id,name',
'items:id,transaction_out_id,product_id,quantity',
'items.product:id,name',
])
->when($request->start_date, function ($q) use ($request) {
$q->where('transaction_date', '>=', $request->start_date);
})
->when($request->end_date, function ($q) use ($request) {
$q->where('transaction_date', '<=', $request->end_date);
})
->orderBy('transaction_date', 'desc')
->paginate(50);
// Transform - no additional queries needed!
$data = $transactions->getCollection()->map(function ($transaction) {
return [
'id' => $transaction->id,
'invoice_number' => $transaction->invoice_number,
'date' => $transaction->transaction_date,
'total' => $transaction->total_amount,
'item_count' => $transaction->items->count(),
'total_qty' => $transaction->items->sum('quantity'),
'products' => $transaction->items->pluck('product.name')->implode(', '),
'cashier' => $transaction->user->name,
];
});
return response()->json([
'data' => $data,
'meta' => [
'current_page' => $transactions->currentPage(),
'last_page' => $transactions->lastPage(),
'per_page' => $transactions->perPage(),
'total' => $transactions->total(),
],
]);
}
Total Queries After Optimization:
1 (transactions with pagination)
+ 1 (users via eager load)
+ 1 (items via eager load)
+ 1 (products via eager load)
────────────────────────
= 4 queries ✅
Additional Optimizations
Add Database Index:
// Migration
Schema::table('transactions_out', function (Blueprint $table) {
$table->index('transaction_date');
// Atau composite index jika sering filter + sort
$table->index(['transaction_date', 'id']);
});
Consider Caching untuk Data yang Jarang Berubah:
// Cache summary data
$summary = Cache::remember('transactions.summary.' . $cacheKey, 3600, function () {
return [...];
});
Use Database Aggregation Instead of PHP:
// Instead of loading all items to count/sum in PHP
$transactions = TransactionOut::query()
->select([
'transactions_out.*',
DB::raw('(SELECT COUNT(*) FROM transaction_out_items WHERE transaction_out_id = transactions_out.id) as items_count'),
DB::raw('(SELECT SUM(quantity) FROM transaction_out_items WHERE transaction_out_id = transactions_out.id) as total_qty'),
])
->with(['user:id,name'])
// ...
Expected Improvement
| Metric | Before | After |
|---|---|---|
| Queries | 351 | 4-5 |
| Typical Response Time | 5-10s | 100-300ms |
| Improvement | - | ~95% faster |
Case 3: Debugging Logic Error
Code jalan tanpa error, tapi hasil salah. Ini yang paling tricky.
Scenario:
Total profit yang dihitung salah. Seharusnya Rp 500.000, tapi system show Rp 350.000.
Prompt:
Profit calculation saya menghasilkan nilai yang salah.
Expected: Rp 500.000
Actual: Rp 350.000
Data test case:
- Product A: Buy price 10.000, Sell price 15.000, Sold 50 units
- Product B: Buy price 20.000, Sell price 30.000, Sold 25 units
Expected calculation:
- Product A profit: (15.000 - 10.000) × 50 = 250.000
- Product B profit: (30.000 - 20.000) × 25 = 250.000
- Total: 500.000
Code saya:
```php
public function calculateProfit(Transaction $transaction): float
{
$profit = 0;
foreach ($transaction->items as $item) {
$product = $item->product;
$itemProfit = ($product->sell_price - $product->buy_price) * $item->quantity;
$profit =+ $itemProfit;
}
return $profit;
}
Saya sudah cek:
- Data di database benar
- Relationship load dengan benar
- sell_price dan buy_price values benar
Apa yang salah?
**Output:**
---
### Bug Found! 🐛
**Line dengan bug:**
```php
$profit =+ $itemProfit;
Seharusnya:
$profit += $itemProfit;
Explanation
=+ dan += adalah dua hal yang SANGAT berbeda:
$profit += $itemProfit (Correct)
- Ini adalah addition assignment operator
- Equivalent to:
$profit = $profit + $itemProfit - Accumulates values
$profit =+ $itemProfit (Your Code)
- Ini adalah assignment dengan unary plus
- Equivalent to:
$profit = (+$itemProfit) - The
+is just a unary plus (positive sign), does nothing to positive numbers - Ini OVERWRITE $profit dengan item terakhir, bukan menambahkan
Why You Got 350.000
Loop 1: Product A
$itemProfit = (15000 - 10000) × 50 = 250000
$profit =+ 250000 → $profit = 250000
Loop 2: Product B
$itemProfit = (30000 - 20000) × 25 = 250000
$profit =+ 250000 → $profit = 250000 (OVERWRITE, not add!)
Final: 250000 (only last item's profit)
Wait, tapi kalian bilang actual result adalah 350.000, bukan 250.000. Ini berarti mungkin ada item ketiga atau ada perhitungan lain.
Regardless, bug-nya adalah =+ vs +=.
Fixed Code
public function calculateProfit(Transaction $transaction): float
{
$profit = 0;
foreach ($transaction->items as $item) {
$product = $item->product;
$itemProfit = ($product->sell_price - $product->buy_price) * $item->quantity;
$profit += $itemProfit; // FIXED: += instead of =+
}
return $profit;
}
Even Better: Use Collection Method
public function calculateProfit(Transaction $transaction): float
{
return $transaction->items->sum(function ($item) {
return ($item->product->sell_price - $item->product->buy_price) * $item->quantity;
});
}
Ini lebih concise dan nggak ada risiko typo =+ vs +=.
Case 4: Environment-Specific Issues
Works di local, error di production. Classic.
Prompt:
Code works perfectly di local tapi error di production:
Error: Class "App\\Services\\TransactionService" not found
Padahal:
- File ada di app/Services/TransactionService.php
- Namespace benar: namespace App\\Services;
- Class name benar: class TransactionService
- Di local jalan perfect
Production environment:
- Laravel 11
- PHP 8.2
- Nginx
- Deployed via Git pull
Apa kemungkinan penyebabnya dan bagaimana fix?
Output:
Kemungkinan Penyebab
1. Composer Autoload Not Updated (Most Common)
Setelah deploy, autoload cache masih pakai versi lama yang belum ada file baru.
Fix:
composer dump-autoload
Atau kalau pakai cached autoload:
composer dump-autoload -o # Optimized
2. Case Sensitivity Issue
Linux (production) is case-sensitive. Mac/Windows (local) is not.
- File:
TransactionService.php - Namespace:
App\\Services\\transactionService(lowercase 't')
Akan work di Mac, fail di Linux.
Check: Verify exact casing matches di filename, namespace, dan use statement.
3. Cached Config/Routes
Laravel cache mungkin masih reference old state.
Fix:
php artisan config:clear
php artisan cache:clear
php artisan route:clear
php artisan view:clear
# Atau nuclear option
php artisan optimize:clear
4. OPcache Caching Old Files
PHP OPcache might serve cached bytecode dari file lama.
Fix:
# Restart PHP-FPM
sudo systemctl restart php8.2-fpm
# Atau di code
opcache_reset();
5. Git Pull Didn't Actually Pull
Maybe ada conflict atau .gitignore issue.
Check:
git status
git log --oneline -5
ls -la app/Services/
6. Symlink/Path Issue
Kalau deploy dengan symlink (seperti Envoyer, Deployer), path might not resolve correctly.
Check:
readlink -f /var/www/app/Services/TransactionService.php
Recommended Deploy Checklist
# After git pull
git pull origin main
# Install dependencies
composer install --no-dev --optimize-autoloader
# Clear all caches
php artisan optimize:clear
# Rebuild caches
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Restart PHP-FPM (if using)
sudo systemctl restart php8.2-fpm
Debug Commands untuk Production
# Check if file actually exists
ls -la app/Services/
# Check file content
head -20 app/Services/TransactionService.php
# Check autoload mapping
composer dump-autoload -v
# Test class loading
php artisan tinker
>>> new \\App\\Services\\TransactionService()
Tips Debugging dengan AI
Include full error message
Stack trace, error code, semuanya. AI butuh context lengkap.
Describe what you already tried
"Sudah coba X, Y, Z tapi masih error" — ini membantu AI eliminate possibilities.
Share relevant code
Jangan cuma bilang "error di TransactionService". Paste code yang relevant.
Describe environment
- Laravel version
- PHP version
- Database type dan version
- Local vs production
- Recent changes
Ask for debug steps, not just solution
Kadang kita butuh tau cara investigate, bukan langsung answer.
Gimana cara debug issue ini step by step?
Saya mau paham prosesnya, bukan cuma fix-nya.
Emergency Debugging Checklist
Ketika production down dan panic mode:
1. Check Logs First
tail -100 storage/logs/laravel.log
tail -100 /var/log/nginx/error.log
2. Check Basic Services
# Database connection
php artisan db:show
# Cache connection
php artisan cache:clear
# Queue status
php artisan queue:status
3. Check Recent Changes
git log --oneline -10
git diff HEAD~1
4. Rollback If Critical
git checkout HEAD~1
composer install
php artisan optimize:clear
5. Document Everything
Setelah fix, catat:
- What was the error
- What caused it
- How it was fixed
- How to prevent in future
Checklist Debugging
- [ ] Error message di-read dan di-understand
- [ ] Relevant code reviewed
- [ ] Steps to reproduce identified
- [ ] Root cause found (bukan cuma symptom)
- [ ] Fix implemented dan tested
- [ ] Sama error nggak akan terjadi lagi (prevention)
- [ ] Documented untuk future reference
Debugging adalah skill yang improve dengan experience. AI mempercepat process dengan memberi perspectives yang mungkin nggak kepikiran. Next, final bagian: documentation dan handover!
Bagian 10: Documentation dan Handover
Project selesai. Semua fitur works. Tests pass. Saatnya handover ke client.
Dan di sinilah banyak freelancer drop the ball.
"Ah, code-nya udah jelas kok." "Nanti kalau ada apa-apa tinggal hubungi saya." "Documentation? Kan ada comments di code."
Wrong mindset.
Documentation yang baik adalah:
- Professional impression — Client lihat kalian serius dan organized
- Self-protection — Kalau ada masalah nanti, documentation jadi reference
- Future-proofing — Kalau client hire developer lain, mereka bisa lanjutin
- Selling point — "Deliverables include comprehensive documentation" bisa jadi differentiator
Dan dengan AI, bikin documentation nggak perlu makan waktu berhari-hari.
Apa Saja yang Perlu Di-dokumentasi?
Untuk Developer (Technical Documentation):
- README.md — Project overview, setup instructions
- API Documentation — Endpoints, request/response format
- Database Schema — ERD, table descriptions
- Architecture Overview — Folder structure, patterns used
- Code Comments — Complex logic explanation
Untuk Client (User Documentation):
- User Guide — How to use the application
- Admin Guide — How to manage the system
- FAQ — Common questions and issues
- Troubleshooting — Common problems and solutions
Untuk Deployment:
- Server Requirements
- Installation Steps
- Configuration Guide
- Maintenance Tasks
Case 1: Generate Professional README
README adalah "first impression" dari codebase. Harus comprehensive tapi scannable.
Prompt:
Buatkan professional README.md untuk Laravel inventory management system dengan detail:
Project Info:
- Nama: Inventory Pro
- Deskripsi: Inventory management system untuk retail store
- Laravel 11, PHP 8.2, MySQL 8, Redis
Features:
- Multi-user dengan roles (Admin, Staff)
- Product management dengan categories
- Transaction In (purchasing dari supplier)
- Transaction Out (sales)
- Stock tracking dengan alerts
- Profit reporting dengan FIFO costing
- Activity logs
Tech Stack:
- Laravel 11
- MySQL 8.0
- Redis (caching & queue)
- Laravel Sanctum (API auth)
- Spatie Laravel-Permission
- Spatie Laravel-Activitylog
- Laravel Excel (export)
Include sections:
1. Project overview dengan badges
2. Features list
3. Requirements
4. Installation (step-by-step detailed)
5. Configuration (.env variables)
6. Database setup
7. Running the application
8. Running tests
9. API documentation overview
10. Folder structure explanation
11. Deployment notes
12. Contributing guidelines (optional)
13. License
14. Contact/Support
Format dengan proper Markdown, include code blocks untuk commands.
Output:
# 📦 Inventory Pro




Inventory management system untuk retail store. Mengelola produk, transaksi pembelian/penjualan, tracking stok, dan laporan profit.
---
## ✨ Features
- **Multi-User Management** — Role-based access control (Admin, Staff)
- **Product Management** — CRUD produk dengan kategori dan foto
- **Stock Tracking** — Real-time stock monitoring dengan low stock alerts
- **Transaction In** — Pencatatan pembelian dari supplier
- **Transaction Out** — Pencatatan penjualan dengan auto-stock reduction
- **Profit Reporting** — Kalkulasi profit dengan metode FIFO
- **Activity Logs** — Audit trail untuk semua perubahan data
- **Export** — Export laporan ke Excel/PDF
- **RESTful API** — Fully documented API untuk integrasi
---
## 📋 Requirements
- PHP 8.2 atau lebih tinggi
- Composer 2.x
- MySQL 8.0 atau lebih tinggi
- Redis 6.x (untuk caching dan queue)
- Node.js 18+ dan NPM (untuk asset compilation, jika ada frontend)
**PHP Extensions yang Dibutuhkan:**
- BCMath
- Ctype
- JSON
- Mbstring
- OpenSSL
- PDO
- Tokenizer
- XML
- Redis
---
## 🚀 Installation
### 1. Clone Repository
```bash
git clone <https://github.com/username/inventory-pro.git>
cd inventory-pro
2. Install Dependencies
# Install PHP dependencies
composer install
# Install Node dependencies (jika ada frontend)
npm install
3. Environment Setup
# Copy environment file
cp .env.example .env
# Generate application key
php artisan key:generate
4. Configure Environment
Edit file .env dan sesuaikan:
# Application
APP_NAME="Inventory Pro"
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8000
# Database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=inventory_pro
DB_USERNAME=root
DB_PASSWORD=your_password
# Redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
# Cache & Session
CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
# Mail (untuk notifications)
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=your_username
MAIL_PASSWORD=your_password
5. Database Setup
# Create database
mysql -u root -p -e "CREATE DATABASE inventory_pro CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# Run migrations
php artisan migrate
# Seed initial data (roles, admin user, sample data)
php artisan db:seed
6. Storage Setup
# Create symbolic link untuk public storage
php artisan storage:link
# Set permissions (Linux/Mac)
chmod -R 775 storage bootstrap/cache
7. Build Assets (jika ada frontend)
npm run build
🏃 Running the Application
Development
# Start Laravel development server
php artisan serve
# Start queue worker (terminal terpisah)
php artisan queue:work
# Watch for asset changes (terminal terpisah, jika ada frontend)
npm run dev
Aplikasi berjalan di: http://localhost:8000
Default Credentials
| Role | Password | |
|---|---|---|
| Admin | [email protected] | password |
| Staff | [email protected] | password |
🧪 Running Tests
# Run all tests
php artisan test
# Run with coverage
php artisan test --coverage
# Run specific test file
php artisan test tests/Feature/TransactionTest.php
# Run specific test method
php artisan test --filter=test_can_create_transaction
📚 API Documentation
API documentation tersedia di: http://localhost:8000/api/documentation
Authentication
API menggunakan Laravel Sanctum. Untuk authenticate:
POST /api/v1/auth/login
Content-Type: application/json
{
"email": "[email protected]",
"password": "password"
}
Response akan berisi token yang digunakan untuk request selanjutnya:
Authorization: Bearer {token}
Endpoint Overview
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/auth/login | Login |
| POST | /api/v1/auth/logout | Logout |
| GET | /api/v1/products | List products |
| POST | /api/v1/products | Create product |
| GET | /api/v1/transactions | List transactions |
| POST | /api/v1/transactions | Create transaction |
| GET | /api/v1/reports/profit | Profit report |
Lihat dokumentasi lengkap di folder /docs/api/.
📁 Folder Structure
inventory-pro/
├── app/
│ ├── Console/ # Artisan commands
│ ├── Events/ # Event classes
│ ├── Exceptions/ # Custom exceptions
│ ├── Http/
│ │ ├── Controllers/ # HTTP controllers
│ │ ├── Middleware/ # HTTP middleware
│ │ ├── Requests/ # Form request validation
│ │ └── Resources/ # API resources
│ ├── Listeners/ # Event listeners
│ ├── Models/ # Eloquent models
│ ├── Services/ # Business logic services
│ └── Traits/ # Reusable traits
├── config/ # Configuration files
├── database/
│ ├── factories/ # Model factories
│ ├── migrations/ # Database migrations
│ └── seeders/ # Database seeders
├── docs/ # Documentation
│ ├── api/ # API documentation
│ └── user-guide/ # User guide
├── public/ # Public assets
├── resources/
│ ├── views/ # Blade templates
│ └── js/ # JavaScript files
├── routes/
│ ├── api.php # API routes
│ └── web.php # Web routes
├── storage/ # Storage (logs, cache, uploads)
└── tests/
├── Feature/ # Feature tests
└── Unit/ # Unit tests
🚢 Deployment
Server Requirements
- PHP 8.2+ dengan extensions yang disebutkan
- Nginx atau Apache
- MySQL 8.0
- Redis
- Supervisor (untuk queue worker)
- SSL Certificate (recommended)
Deployment Steps
# 1. Clone dan install
git clone <https://github.com/username/inventory-pro.git>
cd inventory-pro
composer install --no-dev --optimize-autoloader
# 2. Setup environment
cp .env.example .env
php artisan key:generate
# Edit .env dengan production values
# 3. Optimize
php artisan config:cache
php artisan route:cache
php artisan view:cache
# 4. Database
php artisan migrate --force
php artisan db:seed --class=ProductionSeeder --force
# 5. Storage
php artisan storage:link
chown -R www-data:www-data storage bootstrap/cache
# 6. Setup Supervisor untuk queue
# Lihat docs/deployment/supervisor.conf
Maintenance
# Clear cache
php artisan optimize:clear
# Check logs
tail -f storage/logs/laravel.log
# Backup database
mysqldump -u root -p inventory_pro > backup_$(date +%Y%m%d).sql
🤝 Contributing
- Fork repository
- Create feature branch (
git checkout -b feature/amazing-feature) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open Pull Request
📄 License
This project is licensed under the MIT License — see the LICENSE file for details.
📞 Support
Untuk pertanyaan atau issues:
- Email: [email protected]
- Issue Tracker: GitHub Issues
Built with ❤️ using Laravel
---
### Case 2: Generate User Guide untuk Client
Client biasanya non-technical. Dokumentasi harus dalam bahasa yang mudah dipahami.
**Prompt:**
Buatkan User Guide untuk client (non-technical) dalam Bahasa Indonesia.
Aplikasi: Inventory Pro (sistem manajemen inventory)
Cover hal berikut dengan bahasa sederhana dan step-by-step:
- Pengenalan Aplikasi
- Apa itu Inventory Pro
- Manfaat menggunakan aplikasi ini
- Memulai
- Cara login
- Overview dashboard
- Cara logout dan keamanan
- Mengelola Produk
- Melihat daftar produk
- Menambah produk baru
- Edit produk
- Menghapus/menonaktifkan produk
- Upload foto produk
- Mengelola Kategori
- Melihat kategori
- Menambah kategori
- Mencatat Pembelian (Stok Masuk)
- Kapan digunakan
- Cara mencatat pembelian
- Memilih supplier
- Mencatat Penjualan (Stok Keluar)
- Cara membuat transaksi baru
- Menambah produk ke keranjang
- Memberikan diskon
- Mencetak struk
- Melihat Laporan
- Laporan stok
- Laporan penjualan
- Laporan profit
- Export ke Excel
- Pengaturan (Khusus Admin)
- Mengelola user
- Mengganti password
- FAQ dan Troubleshooting
- Pertanyaan umum
- Masalah umum dan solusinya
Format:
- Bahasa Indonesia yang mudah dipahami
- Step-by-step dengan numbering
- Gunakan istilah yang konsisten
- Sertakan tips dimana relevant
- Jangan terlalu teknis
**Output (partial - beberapa section):**
```markdown
# 📘 Panduan Pengguna Inventory Pro
## Selamat Datang!
Panduan ini akan membantu Anda menggunakan aplikasi **Inventory Pro** untuk mengelola
inventori toko Anda dengan mudah dan efisien.
---
## 1. Pengenalan Aplikasi
### Apa itu Inventory Pro?
Inventory Pro adalah aplikasi untuk mengelola stok barang di toko Anda. Dengan aplikasi ini, Anda bisa:
- ✅ Mencatat semua produk yang dijual
- ✅ Mencatat pembelian barang dari supplier
- ✅ Mencatat penjualan kepada pelanggan
- ✅ Memantau stok secara real-time
- ✅ Mendapat peringatan saat stok menipis
- ✅ Melihat laporan keuntungan
### Siapa yang Menggunakan?
| Role | Akses |
|------|-------|
| **Admin** | Akses penuh ke semua fitur, termasuk kelola user dan laporan lengkap |
| **Staff** | Bisa mencatat transaksi dan melihat stok, tidak bisa kelola user |
---
## 2. Memulai
### 2.1 Cara Login
1. Buka aplikasi di browser (alamat akan diberikan oleh Admin IT)
2. Masukkan **Email** yang terdaftar
3. Masukkan **Password** Anda
4. Klik tombol **Masuk**
> 💡 **Tips:** Centang "Ingat Saya" jika menggunakan komputer pribadi agar tidak perlu login berulang kali.
### 2.2 Halaman Dashboard
Setelah login, Anda akan melihat **Dashboard** yang berisi:
- **Ringkasan Hari Ini** — Total penjualan dan transaksi hari ini
- **Peringatan Stok** — Produk yang stoknya sudah menipis
- **Transaksi Terakhir** — 5 transaksi terakhir yang dicatat
- **Menu Samping** — Untuk navigasi ke fitur lain
### 2.3 Logout
Untuk keluar dari aplikasi:
1. Klik nama Anda di pojok kanan atas
2. Pilih **Keluar**
> ⚠️ **Penting:** Selalu logout jika menggunakan komputer bersama untuk keamanan data.
---
## 3. Mengelola Produk
### 3.1 Melihat Daftar Produk
1. Klik menu **Produk** di sidebar kiri
2. Anda akan melihat tabel berisi semua produk
3. Gunakan **kotak pencarian** untuk mencari produk tertentu
4. Gunakan **filter kategori** untuk melihat produk per kategori
### 3.2 Menambah Produk Baru
1. Klik menu **Produk**
2. Klik tombol **+ Tambah Produk** (warna hijau, di pojok kanan atas)
3. Isi formulir:
- **Nama Produk** — Nama yang mudah dikenali (wajib)
- **SKU** — Kode unik produk, bisa dikosongkan (akan digenerate otomatis)
- **Kategori** — Pilih kategori yang sesuai
- **Harga Beli** — Harga beli dari supplier
- **Harga Jual** — Harga jual ke pelanggan
- **Stok Awal** — Jumlah stok saat ini
- **Minimal Stok** — Batas stok untuk peringatan (misal: 10)
- **Deskripsi** — Keterangan tambahan (opsional)
- **Foto** — Upload foto produk (opsional)
4. Klik **Simpan**
> 💡 **Tips:** Harga jual sebaiknya lebih tinggi dari harga beli agar mendapat keuntungan.
### 3.3 Edit Produk
1. Di daftar produk, cari produk yang ingin diubah
2. Klik ikon **pensil** (✏️) di kolom Aksi
3. Ubah informasi yang diperlukan
4. Klik **Simpan Perubahan**
### 3.4 Menghapus/Menonaktifkan Produk
**Menonaktifkan** (Direkomendasikan):
1. Edit produk
2. Hapus centang pada **Produk Aktif**
3. Simpan
Produk nonaktif tidak muncul saat mencatat transaksi tapi datanya tetap tersimpan.
**Menghapus Permanen:**
1. Klik ikon **tempat sampah** (🗑️)
2. Konfirmasi penghapusan
> ⚠️ **Peringatan:** Produk yang pernah ada di transaksi tidak bisa dihapus permanen. Gunakan fitur nonaktifkan saja.
---
## 6. Mencatat Penjualan (Stok Keluar)
### 6.1 Membuat Transaksi Baru
1. Klik menu **Penjualan** di sidebar
2. Klik tombol **+ Transaksi Baru**
3. Anda akan masuk ke halaman pencatatan penjualan
### 6.2 Menambah Produk ke Keranjang
1. Di kotak **Cari Produk**, ketik nama atau kode produk
2. Pilih produk dari daftar yang muncul
3. Masukkan **jumlah** yang dibeli
4. Klik **Tambah**
5. Ulangi untuk produk lainnya
> 💡 **Tips:** Produk dengan stok 0 akan muncul abu-abu dan tidak bisa ditambahkan.
### 6.3 Memberikan Diskon
1. Di bagian **Ringkasan**, lihat kolom **Diskon**
2. Masukkan nominal diskon (contoh: 10000 untuk Rp 10.000)
3. Total akan otomatis dikurangi diskon
### 6.4 Memproses Transaksi
1. Periksa kembali daftar belanja
2. Klik **Proses Transaksi**
3. Konfirmasi dengan klik **Ya, Proses**
4. Transaksi berhasil! Stok akan otomatis berkurang
### 6.5 Mencetak Struk
Setelah transaksi berhasil:
1. Klik **Cetak Struk** untuk print langsung
2. Atau klik **Download PDF** untuk simpan file
---
## 9. FAQ dan Troubleshooting
### ❓ Pertanyaan Umum
**Q: Bagaimana jika lupa password?**
A: Klik "Lupa Password" di halaman login, masukkan email, dan ikuti instruksi di email.
**Q: Apakah data saya aman?**
A: Ya, semua data disimpan dengan enkripsi dan di-backup secara berkala.
**Q: Bisa diakses dari HP?**
A: Ya, aplikasi bisa dibuka dari browser HP. Tampilan akan menyesuaikan.
**Q: Bagaimana jika stok di sistem tidak sesuai fisik?**
A: Lakukan stock opname. Masuk ke menu **Stok > Penyesuaian** dan sesuaikan angkanya.
### 🔧 Masalah Umum
**Masalah: Tidak bisa login**
- Pastikan email dan password benar
- Pastikan Caps Lock tidak aktif
- Coba refresh halaman (tekan F5)
- Jika masih gagal, hubungi Admin
**Masalah: Halaman loading terus**
- Periksa koneksi internet
- Coba refresh halaman
- Clear cache browser (Ctrl + Shift + Delete)
**Masalah: Produk tidak muncul saat cari**
- Pastikan ejaan benar
- Coba cari dengan kode SKU
- Pastikan produk tidak dinonaktifkan
**Masalah: Tidak bisa menghapus produk**
- Produk yang sudah ada di transaksi tidak bisa dihapus
- Solusi: Nonaktifkan saja produk tersebut
---
## 📞 Butuh Bantuan?
Jika mengalami masalah yang tidak ada di panduan ini:
- **Email:** [email protected]
- **WhatsApp:** 0812-xxxx-xxxx
- **Jam Operasional:** Senin - Jumat, 09:00 - 17:00 WIB
Case 3: Generate Inline Code Comments
Code yang complex perlu explanation. Tapi nulis comments itu tedious.
Prompt:
Tambahkan comprehensive PHPDoc comments dan inline comments untuk class berikut.
Focus pada:
- Class-level documentation
- Method descriptions dengan @param dan @return
- Complex logic explanation
- @throws untuk exceptions
- Example usage di docblock untuk public methods
```php
<?php
namespace App\\Services;
use App\\Models\\Product;
use App\\Models\\TransactionOut;
use Illuminate\\Support\\Collection;
use Illuminate\\Support\\Facades\\DB;
class TransactionService
{
use GeneratesInvoiceNumber;
public function createSaleTransaction(array $data, int $userId): TransactionOut
{
$preparedItems = $this->prepareAndValidateItems($data['items']);
$totals = $this->calculateTotals($preparedItems, $data['discount'] ?? 0);
return DB::transaction(function () use ($data, $userId, $preparedItems, $totals) {
$this->lockAndRevalidateStock($preparedItems);
$transaction = $this->createTransaction($data, $userId, $totals);
$this->createItemsAndReduceStock($transaction, $preparedItems);
$transaction->load(['items.product', 'user']);
event(new TransactionCreated($transaction, $data));
return $transaction;
});
}
protected function prepareAndValidateItems(array $items): Collection
{
return collect($items)->map(function ($item) {
$product = Product::find($item['product_id']);
if (!$product) {
throw new ProductNotAvailableException("Product not found");
}
if (!$product->is_active) {
throw new ProductNotAvailableException("Product {$product->name} is not active");
}
if ($product->stock < $item['quantity']) {
throw new InsufficientStockException(
"Insufficient stock for {$product->name}. Available: {$product->stock}"
);
}
return [
'product' => $product,
'quantity' => $item['quantity'],
'price' => $product->sell_price,
'subtotal' => $product->sell_price * $item['quantity'],
];
});
}
}
**Output:**
```php
<?php
namespace App\\Services;
use App\\Events\\TransactionCreated;
use App\\Exceptions\\InsufficientStockException;
use App\\Exceptions\\ProductNotAvailableException;
use App\\Models\\Product;
use App\\Models\\TransactionOut;
use App\\Traits\\GeneratesInvoiceNumber;
use Illuminate\\Support\\Collection;
use Illuminate\\Support\\Facades\\DB;
use InvalidArgumentException;
/**
* Service class for handling sale transaction business logic.
*
* This service encapsulates all the logic required to create sale transactions,
* including validation, stock management, and database operations. It ensures
* data integrity through database transactions and proper locking mechanisms.
*
* @package App\\Services
* @author Your Name
*
* @example
* // Basic usage
* $service = new TransactionService();
* $transaction = $service->createSaleTransaction([
* 'items' => [
* ['product_id' => 1, 'quantity' => 2],
* ['product_id' => 2, 'quantity' => 1],
* ],
* 'discount' => 10000,
* 'notes' => 'Regular customer discount',
* ], auth()->id());
*/
class TransactionService
{
use GeneratesInvoiceNumber;
/**
* Create a new sale transaction with the given items.
*
* This method orchestrates the entire transaction creation process:
* 1. Validates all items (product existence, active status, stock availability)
* 2. Calculates totals and applies discount
* 3. Executes database operations within a transaction for data integrity
* 4. Reduces stock for each product
* 5. Dispatches event for post-processing (notifications, logging, etc.)
*
* The method uses database locking to prevent race conditions when multiple
* transactions are created simultaneously for the same products.
*
* @param array $data Transaction data containing:
* - items: array of ['product_id' => int, 'quantity' => int]
* - discount: float (optional, default 0)
* - notes: string (optional)
* - customer_name: string (optional)
* @param int $userId The ID of the user creating the transaction
*
* @return TransactionOut The created transaction with items and user loaded
*
* @throws ProductNotAvailableException When product doesn't exist or is inactive
* @throws InsufficientStockException When requested quantity exceeds available stock
* @throws InvalidArgumentException When discount exceeds subtotal
* @throws \\Throwable When database operation fails (transaction will be rolled back)
*
* @example
* try {
* $transaction = $service->createSaleTransaction([
* 'items' => [
* ['product_id' => 1, 'quantity' => 5],
* ],
* 'discount' => 5000,
* ], $userId);
* echo "Created: " . $transaction->invoice_number;
* } catch (InsufficientStockException $e) {
* echo "Stock tidak cukup: " . $e->getMessage();
* }
*/
public function createSaleTransaction(array $data, int $userId): TransactionOut
{
// Step 1: Prepare and validate all items before starting transaction
// This validates product existence, active status, and stock availability
$preparedItems = $this->prepareAndValidateItems($data['items']);
// Step 2: Calculate totals (subtotal, apply discount, final total)
// Throws InvalidArgumentException if discount > subtotal
$totals = $this->calculateTotals($preparedItems, $data['discount'] ?? 0);
// Step 3: Execute all database operations in a transaction
// If any step fails, all changes are rolled back automatically
return DB::transaction(function () use ($data, $userId, $preparedItems, $totals) {
// Lock products to prevent race condition
// Another transaction trying to modify these products will wait
$this->lockAndRevalidateStock($preparedItems);
// Create the main transaction record
$transaction = $this->createTransaction($data, $userId, $totals);
// Create transaction items and reduce stock atomically
$this->createItemsAndReduceStock($transaction, $preparedItems);
// Eager load relationships for the response
$transaction->load(['items.product', 'user']);
// Dispatch event for side effects (notifications, activity log, etc.)
// These are handled asynchronously by listeners
event(new TransactionCreated($transaction, $data));
return $transaction;
});
}
/**
* Prepare and validate transaction items.
*
* This method transforms raw item input into a validated collection with
* product details and calculated subtotals. It performs three validations:
* 1. Product must exist in database
* 2. Product must be active (is_active = true)
* 3. Requested quantity must not exceed available stock
*
* Note: This is an initial validation. Stock is revalidated after locking
* in the database transaction to handle race conditions.
*
* @param array $items Raw items array, each containing:
* - product_id: int
* - quantity: int
*
* @return Collection Collection of prepared items, each containing:
* - product: Product model instance
* - quantity: int
* - price: float (sell_price at time of transaction)
* - subtotal: float (price × quantity)
*
* @throws ProductNotAvailableException When product not found or inactive
* @throws InsufficientStockException When stock is insufficient
*/
protected function prepareAndValidateItems(array $items): Collection
{
return collect($items)->map(function ($item) {
// Find product - returns null if not found
$product = Product::find($item['product_id']);
// Validation 1: Product must exist
if (!$product) {
throw new ProductNotAvailableException(
"Product with ID {$item['product_id']} not found"
);
}
// Validation 2: Product must be active
// Inactive products cannot be sold but remain in database for history
if (!$product->is_active) {
throw new ProductNotAvailableException(
"Product {$product->name} is not active"
);
}
// Validation 3: Stock must be sufficient
// Note: This is preliminary check; stock is rechecked after locking
if ($product->stock < $item['quantity']) {
throw new InsufficientStockException(
"Insufficient stock for {$product->name}. " .
"Available: {$product->stock}, Requested: {$item['quantity']}"
);
}
// Return prepared item with current price (snapshot)
// Price is captured at transaction time, not referenced
return [
'product' => $product,
'quantity' => (int) $item['quantity'],
'price' => $product->sell_price,
'subtotal' => $product->sell_price * $item['quantity'],
];
});
}
/**
* Calculate transaction totals with discount.
*
* @param Collection $items Prepared items collection
* @param float $discount Discount amount (absolute value, not percentage)
*
* @return array{subtotal: float, discount: float, total: float}
*
* @throws InvalidArgumentException When discount exceeds subtotal
*/
protected function calculateTotals(Collection $items, float $discount): array
{
$subtotal = $items->sum('subtotal');
// Discount cannot exceed subtotal (no negative total)
if ($discount > $subtotal) {
throw new InvalidArgumentException(
"Discount ({$discount}) cannot exceed subtotal ({$subtotal})"
);
}
return [
'subtotal' => $subtotal,
'discount' => $discount,
'total' => $subtotal - $discount,
];
}
/**
* Lock products and revalidate stock availability.
*
* Uses SELECT ... FOR UPDATE to lock product rows, preventing other
* transactions from modifying them until this transaction completes.
* This prevents race conditions where two transactions might oversell
* the same product.
*
* @param Collection $items Prepared items collection
*
* @throws InsufficientStockException If stock changed since initial validation
*/
protected function lockAndRevalidateStock(Collection $items): void
{
// Extract product IDs for locking
$productIds = $items->pluck('product.id');
// Lock rows - other transactions will wait here
$lockedProducts = Product::whereIn('id', $productIds)
->lockForUpdate()
->get()
->keyBy('id');
// Revalidate stock with locked (current) values
foreach ($items as $item) {
$product = $lockedProducts->get($item['product']->id);
if ($product->stock < $item['quantity']) {
throw new InsufficientStockException(
"Stock changed for {$product->name}. " .
"Now available: {$product->stock}"
);
}
}
}
// ... additional methods with similar documentation
}
Handover Checklist
Sebelum handover ke client, pastikan:
Code Quality:
- [ ] All tests passing
- [ ] No critical bugs
- [ ] Code reviewed
- [ ] Linting clean
Documentation:
- [ ] README.md lengkap dan up-to-date
- [ ] API documentation tersedia
- [ ] User guide untuk end users
- [ ] Deployment guide
Repository:
- [ ] Clean git history
- [ ] No sensitive data di repo
- [ ] .env.example lengkap
- [ ] Proper .gitignore
Deliverables:
- [ ] Source code (zip atau repo access)
- [ ] Database dump (structure + sample data)
- [ ] Documentation files
- [ ] Login credentials (secure channel)
Knowledge Transfer:
- [ ] Walkthrough session dengan client (optional tapi recommended)
- [ ] Contact info untuk support
- [ ] Scope of support yang jelas
Tips Documentation dengan AI
Batch generate documentation
Generate documentation untuk semua 15 API endpoints di folder app/Http/Controllers/Api/
Format: satu tabel overview + detail per endpoint
Convert format
Convert README ini ke format PDF yang professional untuk dikirim ke client.
Include cover page dengan logo placeholder.
Simplify technical content
Jelaskan konsep ini untuk non-technical client:
[paste technical explanation]
Buat lebih sederhana, pakai analogi kalau perlu.
Generate from code
Berdasarkan controller ini, generate API documentation yang mencakup:
- Endpoint URL
- HTTP method
- Request parameters
- Response format
- Example request/response
🎉 Penutup
Selamat! Kalian udah sampai di akhir artikel yang panjang ini.
Let's recap 10 cara AI membantu freelance projek Laravel:
| # | Cara | Impact |
|---|---|---|
| 1 | Analisa Brief | Scope jelas, estimasi akurat |
| 2 | Design ERD | Database solid dari awal |
| 3 | API Contract | Expectation clear, development terarah |
| 4 | User Flow | UX terpikirkan, edge cases caught |
| 5 | Scaffolding | Boilerplate cepat selesai |
| 6 | Business Logic | Complex problems solved faster |
| 7 | Refactoring | Code cleaner, maintainable |
| 8 | Testing | Confidence in delivery |
| 9 | Debugging | Issues resolved faster |
| 10 | Documentation | Professional handover |
Key Takeaways
AI adalah tool, bukan pengganti
AI generate, human validate. AI suggest, human decide. AI speed up, human ensure quality.
Jangan blindly accept AI output. Review, understand, dan adjust sesuai context.
Build your prompt library
Setiap prompt yang works well, simpan. Over time, kalian punya collection of prompts yang teruji untuk berbagai situasi.
Context is everything
AI output quality = input quality. Semakin detail context yang dikasih, semakin relevant output-nya.
Start small
Nggak perlu langsung pakai semua 10 cara. Mulai dengan 2-3 yang paling impactful untuk workflow kalian. Setelah comfortable, expand.
Real Impact untuk Freelancer
Dengan AI sebagai partner:
- Projek selesai lebih cepat — Boilerplate dan documentation nggak lagi bottleneck
- Kualitas lebih konsisten — Patterns dan best practices ter-apply
- Bisa handle lebih banyak projek — Efficiency meningkat
- Rate bisa lebih tinggi — Deliverables lebih comprehensive
- Stress berkurang — Ada "partner" untuk brainstorm dan problem-solve
What's Next?
Practice dengan projek nyata
Teori udah dapat. Sekarang apply ke projek freelance kalian yang berikutnya.
Explore beyond this article
AI bisa bantu banyak hal lain:
- Email ke client
- Proposal writing
- Contract drafting
- Time estimation
- Code review
Stay updated
AI tools evolving fast. Claude, GPT, dan tools lain terus improve. Keep experimenting.
Share your experience
Punya prompt yang works well? Workflow yang efficient? Share ke community. Kita semua belajar bareng.
🚀 Mau Belajar Laravel Lebih Dalam?
Artikel ini baru permukaannya. Kalau kalian serius mau jadi Laravel developer yang siap kerja — baik freelance maupun remote — kalian butuh pembelajaran yang terstruktur dan hands-on.
Di BuildWithAngga, kalian bisa belajar Laravel dari nol sampai production-ready:
📐 Database Design & ERD Belajar design database yang scalable. Dari normalization, relationships, sampai optimization. Fondasi yang kuat = development lebih lancar.
💻 Laravel Development Mulai dari basic CRUD sampai advanced patterns. Authentication, API development, payment integration, real-time features — semua dibahas dengan studi kasus nyata yang bisa langsung dipraktekkan.
🚢 Deployment & DevOps Code bagus tapi nggak bisa deploy = percuma. Belajar deploy ke VPS, setup CI/CD, SSL, monitoring, dan maintenance. Skill yang sering di-skip tapi crucial untuk freelancer.
🌍 Bekerja Remote Skill teknis udah oke, terus gimana dapat kerjaan remote? Di BuildWithAngga juga dibahas:
- Cara build portfolio yang menarik
- Gimana approach client international
- Platform freelance yang worth it
- Rate negotiation
- Communication best practices
Kenapa BuildWithAngga?
✅ Kurikulum up-to-date (Laravel 12, PHP 8.3+) ✅ Project-based learning — langsung praktek, bukan cuma teori ✅ Mentor yang aktif di industri ✅ Community yang supportive ✅ Lifetime access ke materi ✅ Sertifikat untuk portfolio
Kalian udah baca artikel 10 bagian ini sampai habis. Itu bukti kalian serius. Sekarang tinggal take the next step.
Mulai belajar sekarang di BuildWithAngga.com dan transform dari "bisa Laravel" jadi "Laravel developer yang dicari client".
See you di kelas! 👋
Terima kasih udah baca sampai akhir! Semoga artikel ini membantu journey freelance Laravel kalian.
Sekarang, go build something awesome! 🚀