# Database Reference

> Format: `table_name` → `model_name` | columns | relationships

---

## Auth & User

### `users` → User
`id` `email`(unique) `username`(unique) `password` `is_active`(bool) `last_login_at`(datetime) `created_at` `updated_at` `deleted_at`
- Casts: email_verified_at(datetime), is_active(bool), last_login_at(datetime), password(hashed), two_factor_confirmed_at(datetime)
- Scopes: active()
- Relations: userProfile(HasOne→UserProfile), employee(HasOne→Employee), attendances(HasMany→Attendance), cashAccounts(HasMany→CashAccount,created_by_id), cashTransactions(HasMany→CashTransaction,created_by_id), createdCuttings(HasMany→Cutting,created_by_id), submittedCuttings(HasMany→Cutting,submitted_by_id), createdExpenses(HasMany→Expense,created_by_id), createdOrders(HasMany→Order,created_by_id), marketingOrders(HasMany→Order,marketing_id), orderItems(HasMany→OrderItem), createdPurchases(HasMany→Purchase,created_by_id), createdRestocks(HasMany→Restock,created_by_id), stokOpnamesCreated(HasMany→StokOpname,created_by_id), stokOpnamesVerified(HasMany→StokOpname,verified_by_id), employeeAdvancesPaid(HasMany→EmployeeAdvance,paid_by_id), employeeAdvancesVerified(HasMany→EmployeeAdvance,verified_by_id), paidPayrolls(HasMany→Payroll,paid_by_id), payrollPeriodsClosed(HasMany→PayrollPeriod,closed_by_id), notifications(HasMany→AppNotification), rejections(HasMany→Rejection,rejected_by_id), pushSubscriptions(HasMany→PushSubscription,morph)

### `user_profiles` → UserProfile
`id` `user_id`(FK→users,unique) `full_name`(200) `phone_number`(20,null) `gender`(enum,null) `birth_date`(date,null) `address`(text,null) `created_at` `updated_at` `deleted_at`
- Casts: gender(Gender), birth_date(date:Y-m-d)
- Scopes: female(), male(), hasPhoneNumber()
- Relations: user(BelongsTo→User)

### `push_subscriptions` → PushSubscription
`id` `subscribable_type` `subscribable_id` `endpoint`(500,unique) `public_key`(null) `auth_token`(null) `content_encoding`(null) `created_at` `updated_at`
- Relations: user(MorphTo)

### `notifications` → AppNotification
`id` `user_id`(FK→users) `title` `body`(text,null) `url`(null) `is_read`(bool,default:false) `read_at`(datetime,null) `created_at` `updated_at`
- Casts: is_read(bool), read_at(datetime)
- Accessor: formatted_read_at → 'l, d F Y H:i'
- Relations: user(BelongsTo→User)

---

## Master Data

### `categories` → Category
`id` `name`(50) `slug`(50,unique) `created_at` `updated_at` `deleted_at`
- Relations: products(BelongsToMany→Product via product_categories)
- Accessor: formattedName → ucfirst(name)

### `product_categories` → Category (Pivot)
`product_id`(FK→products) `category_id`(FK→categories)
- No id, no timestamps
- Relations: category(BelongsTo→Category), product(BelongsTo→Product)

### `products` → Product
`id` `name`(200) `slug`(200,unique) `description`(text,null) `status`(enum,default:active) `rejection_reason`(text,null) `created_at` `updated_at` `deleted_at`
- Casts: status(ProductStatus)
- Scopes: active(), draft(), inactive(), pending(), rejected()
- Relations: categories(BelongsToMany→Category), productVariants(HasMany→ProductVariant)

### `product_variants` → ProductVariant
`id` `product_id`(FK→products) `name`(200) `stock`(uint,default:0) `reject_stock`(uint,default:0) `retail_stock`(uint,default:0) `created_at` `updated_at` `deleted_at`
- Casts: stock(int), reject_stock(int), retail_stock(int)
- GlobalScope: orderBy(name)
- Relations: product(BelongsTo→Product), productPrices(HasMany→ProductPrice,variant_id), orderItems(HasMany→OrderItem), restockItems(HasMany→RestockItem), retailStockHistories(HasMany→RetailStockHistory), stokOpnameItems(HasMany→StokOpnameItem), stockMutations(HasMany→StockMutation,morph)

### `product_prices` → ProductPrice
`id` `variant_id`(FK→product_variants) `type`(enum) `price`(uint) `created_at` `updated_at`
- UNIQUE(variant_id, type)
- Casts: type(PriceType), price(int)
- Scopes: retail(), wholesale()
- Relations: variant(BelongsTo→ProductVariant)

### `customers` → Customer
`id` `name`(200) `phone_number`(20,null) `address`(text,null) `created_at` `updated_at` `deleted_at`
- Relations: orders(HasMany→Order)
- Accessor: formattedPhoneNumber (get: "0821 2121 2121", set: strip whitespace)

### `suppliers` → Supplier
`id` `name`(200) `phone_number`(20,null) `address`(text,null) `created_at` `updated_at` `deleted_at`
- Relations: purchases(HasMany→Purchase)
- Accessor: formattedPhoneNumber (get: "0821 2121 2121", set: strip whitespace)

### `raw_materials` → RawMaterial
`id` `name`(200) `unit`(enum) `is_active`(bool,default:true) `created_at` `updated_at` `deleted_at`
- Casts: unit(RawMaterialUnit), is_active(bool)
- Scopes: active(), nonactive(), kg(), meter(), yard()
- Relations: rawMaterialPrices(HasMany→RawMaterialPrice)

### `raw_material_prices` → RawMaterialPrice
`id` `raw_material_id`(FK→raw_materials) `variant`(200) `price`(uint) `stock`(uint,default:0) `created_at` `updated_at` `deleted_at`
- Casts: price(int), stock(int)
- GlobalScope: orderBy(variant)
- Relations: rawMaterial(BelongsTo→RawMaterial,withTrashed), cuttingMaterials(HasMany→CuttingMaterial), purchaseItems(HasMany→PurchaseItem)
- Accessor: photo_url → first media presigned S3 URL

---

## Finance

### `cash_accounts` → CashAccount
`id` `created_by_id`(FK→users) `name`(200) `balance`(ubig,default:0) `created_at` `updated_at` `deleted_at`
- Casts: balance(int)
- Relations: cashTransactions(HasMany→CashTransaction), createdBy(BelongsTo→User)

### `cash_transactions` → CashTransaction
`id` `cash_account_id`(FK→cash_accounts) `created_by_id`(FK→users) `reference_id`(ubig,null,morph) `reference_type`(string,null,morph) `amount`(ubig) `balance_after`(ubig) `type`(enum,default:deposit) `description`(100) `created_at` `updated_at` `deleted_at`
- Casts: type(CashTransactionType), amount(int), balance_after(int)
- Scopes: deposit(), expenseType(), transfer(), withdrawal()
- Relations: cashAccount(BelongsTo→CashAccount), createdBy(BelongsTo→User), reference(MorphTo), expense(HasOne→Expense), order(HasOne→Order), employeeAdvances(HasMany→EmployeeAdvance), payrolls(HasMany→Payroll)

### `expenses` → Expense
`id` `cash_transaction_id`(FK→cash_transactions,unique,null) `created_by_id`(FK→users) `amount`(ubig) `description`(100) `created_at` `updated_at` `deleted_at`
- Casts: amount(int), date(date:Y-m-d)
- Relations: cashTransaction(BelongsTo→CashTransaction), createdBy(BelongsTo→User)

### `employee_advances` → EmployeeAdvance
`id` `paid_by_id`(FK→users,null) `employee_id`(FK→employees) `verified_by_id`(FK→users,null) `repayment_cash_transaction_id`(FK→cash_transactions,unique,null) `cash_transaction_id`(FK→cash_transactions,unique,null) `amount`(ubig) `paid_amount`(ubig,default:0) `description`(100) `due_date`(date) `status`(enum,default:pending) `verified_at`(datetime,null) `paid_at`(datetime,null) `created_at` `updated_at` `deleted_at`
- Casts: status(EmployeeAdvanceStatus), amount(int), paid_amount(int), due_date(date:Y-m-d), verified_at(datetime), paid_at(datetime)
- Scopes: approved(), cancelled(), paid(), pending(), rejected()
- Relations: employee(BelongsTo→Employee), cashTransaction(BelongsTo→CashTransaction), repaymentCashTransaction(BelongsTo→CashTransaction), paidBy(BelongsTo→User), verifiedBy(BelongsTo→User), payments(HasMany→EmployeeAdvancePayment)

### `employee_advance_payments` → EmployeeAdvancePayment
`id` `employee_advance_id`(FK→employee_advances) `paid_by_id`(FK→users,null) `cash_transaction_id`(FK→cash_transactions,unique,null) `amount`(ubig) `description`(100,null) `paid_at`(datetime) `created_at` `updated_at`
- No soft deletes
- Casts: amount(int), paid_at(datetime)
- Accessor: formatted_amount → 'Rp X.XXX', formatted_paid_at → 'l, d F Y H:i'
- Relations: employeeAdvance(BelongsTo→EmployeeAdvance), paidBy(BelongsTo→User), cashTransaction(BelongsTo→CashTransaction)

---

## HR

### `employees` → Employee
`id` `user_id`(FK→users,unique) `join_date`(date) `resign_date`(date,null) `employment_status`(enum,default:full_time) `base_salary`(uint) `created_at` `updated_at` `deleted_at`
- Casts: employment_status(EmploymentStatus), base_salary(int), join_date(date:Y-m-d), resign_date(date:Y-m-d)
- Scopes: contract(), fullTime(), internship(), partTime(), resigned()
- Relations: user(BelongsTo→User), attendances(HasMany→Attendance), employeeAdvances(HasMany→EmployeeAdvance), leaveRequests(HasMany→LeaveRequest), payrolls(HasMany→Payroll)

### `attendances` → Attendance
`id` `employee_id`(FK→employees) `attendance_date`(date) `check_in_at`(datetime) `check_out_at`(datetime,null) `check_in_latitude`(decimal(10,7),null) `check_in_longitude`(decimal(10,7),null) `check_out_latitude`(decimal(10,7),null) `check_out_longitude`(decimal(10,7),null) `work_duration_minutes`(uint,null) `created_at` `updated_at`
- No soft deletes
- Casts: attendance_date(date:Y-m-d), check_in_at(datetime), check_out_at(datetime), check_in/out_latitude/longitude(decimal:7), work_duration_minutes(int)
- Accessor: formatted_attendance_date → 'l, d F Y', formatted_check_in_at → 'l, d F Y H:i', formatted_check_out_at → 'l, d F Y H:i'
- Relations: employee(BelongsTo→Employee), payrollAdjustments(HasMany→PayrollAdjustment)

### `payroll_periods` → PayrollPeriod
`id` `closed_by_id`(FK→users,null) `year`(usmall) `month`(utiny) `status`(enum,default:open) `closed_at`(datetime,null) `created_at` `updated_at` `deleted_at`
- Casts: status(PayrollPeriodStatus), year(int), month(int), closed_at(datetime)
- Accessor: formatted_closed_at → 'l, d F Y H:i', month_name → 'Januari', status_label
- Scopes: closed(), open()
- Relations: closedBy(BelongsTo→User), payrolls(HasMany→Payroll)

### `payrolls` → Payroll
`id` `payroll_period_id`(FK→payroll_periods) `employee_id`(FK→employees) `cash_transaction_id`(FK→cash_transactions,unique,null) `paid_by_id`(FK→users,null) `base_salary`(uint) `bonus_amount`(ubig,default:0) `deduction_amount`(ubig,default:0) `total_amount`(ubig) `status`(enum,default:unpaid) `paid_at`(datetime,null) `created_at` `updated_at` `deleted_at`
- Casts: status(PayrollStatus), base_salary(int), bonus_amount(int), deduction_amount(int), total_amount(int), paid_at(datetime)
- Scopes: cancelled(), paid(), unpaid()
- Relations: payrollPeriod(BelongsTo→PayrollPeriod), employee(BelongsTo→Employee), cashTransaction(BelongsTo→CashTransaction), paidBy(BelongsTo→User), payrollAdjustments(HasMany→PayrollAdjustment)

### `payroll_adjustments` → PayrollAdjustment
`id` `payroll_id`(FK→payrolls) `attendance_id`(FK→attendances,null) `created_by_id`(FK→users) `type`(enum) `amount`(ubig) `description`(100) `created_at` `updated_at` `deleted_at`
- Casts: type(PayrollAdjustmentType), amount(int)
- Scopes: bonus(), deduction()
- Relations: payroll(BelongsTo→Payroll), attendance(BelongsTo→Attendance), createdBy(BelongsTo→User)

### `leave_requests` → LeaveRequest
`id` `employee_id`(FK→employees) `verified_by_id`(FK→users,null) `start_date`(date) `end_date`(date) `total_days`(uint) `status`(enum,default:pending) `verified_at`(datetime,null) `created_at` `updated_at` `deleted_at`
- Casts: status(LeaveRequestStatus), start_date(date:Y-m-d), end_date(date:Y-m-d), total_days(int), verified_at(datetime)
- Scopes: approved(), cancelled(), pending(), rejected()
- Relations: employee(BelongsTo→Employee), verifiedBy(BelongsTo→User)

---

## Sales

### `orders` → Order
`id` `customer_id`(FK→customers,null) `marketing_id`(FK→users,null) `cash_transaction_id`(FK→cash_transactions,unique,null) `created_by_id`(FK→users) `order_number`(30,unique) `channel`(enum) `price_type`(enum) `status`(enum,default:pending) `payment_type`(enum,default:cash) `is_affiliate`(bool,default:false) `tiktok_order_id`(100,null) `shopee_order_id`(100,null) `subtotal`(ubig) `discount`(ubig,default:0) `nego_price`(ubig,null) `marketplace_settings_snapshot`(json,null) `total_amount`(ubig) `cogs`(ubig,default:0) `notes`(text,null) `created_at` `updated_at` `deleted_at`
- Casts: channel(OrderChannel), price_type(PriceType), status(OrderStatus), payment_type(PaymentType), is_affiliate(bool), subtotal(int), discount(int), nego_price(int), total_amount(int), cogs(int), marketplace_settings_snapshot(array)
- Scopes: cancelled(), cash(), completed(), pending(), processing(), qris(), refunded(), retail(), shopee(), store(), tiktok(), transfer(), wholesale()
- Relations: cashTransaction(BelongsTo→CashTransaction), createdBy(BelongsTo→User), customer(BelongsTo→Customer), marketing(BelongsTo→User), orderItems(HasMany→OrderItem)

### `order_items` → OrderItem
`id` `order_id`(FK→orders,null) `user_id`(FK→users,null) `product_variant_id`(FK→product_variants) `stock_quality`(enum,default:good) `quantity`(uint) `unit_price`(ubig) `subtotal`(ubig) `created_at` `updated_at` `deleted_at`
- Casts: stock_quality(ProductStockQuality), quantity(int), unit_price(int), subtotal(int)
- Scopes: good(), reject()
- Relations: order(BelongsTo→Order), productVariant(BelongsTo→ProductVariant), user(BelongsTo→User)

### `rejections` → Rejection
`id` `rejectable_type`(string,null,morph) `rejectable_id`(ubig,null,morph) `rejected_by_id`(FK→users) `reason`(500) `created_at` `updated_at` `deleted_at`
- Relations: rejectable(MorphTo), rejectedBy(BelongsTo→User)

---

## Production

### `cuttings` → Cutting
`id` `created_by_id`(FK→users) `submitted_by_id`(FK→users,null) `status`(enum,default:in_progress) `description`(100,null) `total_material_cost`(ubig,null) `sewing_cost`(ubig,default:0) `other_cost`(ubig,default:0) `cost_per_unit`(ubig,null) `created_at` `updated_at` `deleted_at`
- Casts: status(CuttingStatus), total_material_cost(int), cost_per_unit(int), sewing_cost(int), other_cost(int)
- Scopes: cancelled(), completed(), inProgress()
- Relations: createdBy(BelongsTo→User), submittedBy(BelongsTo→User), cuttingMaterialCombinations(HasMany→CuttingMaterialCombination), cuttingMaterials(HasMany→CuttingMaterial), cuttingResults(HasMany→CuttingResult)

### `cutting_material_combinations` → CuttingMaterialCombination
`id` `user_id`(FK→users,null) `cutting_id`(FK→cuttings,null) `material_result`(int,null) `created_at` `updated_at` `deleted_at`
- Casts: material_result(int)
- Accessor: formatted_material_result → 'X.XXX'
- Relations: cutting(BelongsTo→Cutting), cuttingMaterials(HasMany→CuttingMaterial,combination_id), user(BelongsTo→User)

### `cutting_materials` → CuttingMaterial
`id` `user_id`(FK→users,null) `cutting_id`(FK→cuttings,null) `raw_material_price_id`(FK→raw_material_prices) `combination_id`(FK→cutting_material_combinations,null) `material_usage`(int) `material_result`(int,null) `created_at` `updated_at` `deleted_at`
- Casts: material_usage(int), material_result(int)
- Accessor: formatted_material_result → 'X.XXX', formatted_material_usage → 'X.XXX'
- Relations: cutting(BelongsTo→Cutting), rawMaterialPrice(BelongsTo→RawMaterialPrice), combination(BelongsTo→CuttingMaterialCombination), user(BelongsTo→User)

### `cutting_results` → CuttingResult
`id` `user_id`(FK→users,null) `cutting_id`(FK→cuttings,null) `product_name`(255,null) `cutting_result`(uint,null) `sample`(uint,null) `original_outside_sample`(uint,null) `created_at` `updated_at` `deleted_at`
- Casts: cutting_result(int), sample(int), original_outside_sample(int)
- Accessor: formatted_cutting_result → 'X.XXX', formatted_original_outside_sample → 'X.XXX', formatted_sample → 'X.XXX'
- Relations: cutting(BelongsTo→Cutting), user(BelongsTo→User)

---

## Inventory

### `purchases` → Purchase
`id` `supplier_id`(FK→suppliers) `created_by_id`(FK→users) `subtotal`(ubig) `discount`(ubig,default:0) `shipping_cost`(ubig,default:0) `total`(ubig) `notes`(100,null) `created_at` `updated_at` `deleted_at`
- Casts: subtotal(int), discount(int), shipping_cost(int), total(int)
- Relations: createdBy(BelongsTo→User), supplier(BelongsTo→Supplier), purchaseItems(HasMany→PurchaseItem)

### `purchase_items` → PurchaseItem
`id` `purchase_id`(FK→purchases,null) `user_id`(FK→users,null) `raw_material_price_id`(FK→raw_material_prices) `quantity`(uint) `unit_price`(ubig) `subtotal`(ubig) `created_at` `updated_at` `deleted_at`
- Casts: quantity(int), unit_price(int), subtotal(int)
- Relations: purchase(BelongsTo→Purchase), rawMaterialPrice(BelongsTo→RawMaterialPrice,withTrashed), user(BelongsTo→User)

### `restocks` → Restock
`id` `created_by_id`(FK→users) `subtotal`(ubig) `total`(ubig) `notes`(100,null) `stock_type`(enum,default:good) `created_at` `updated_at` `deleted_at`
- Casts: stock_type(ProductStockQuality), subtotal(int), total(int)
- Scopes: good(), reject()
- Relations: createdBy(BelongsTo→User), restockItems(HasMany→RestockItem)

### `restock_items` → RestockItem
`id` `restock_id`(FK→restocks,null) `user_id`(FK→users,null) `product_variant_id`(FK→product_variants) `quantity`(int) `unit_price`(ubig) `subtotal`(ubig) `created_at` `updated_at` `deleted_at`
- Casts: quantity(int), unit_price(int), subtotal(int)
- Relations: restock(BelongsTo→Restock), productVariant(BelongsTo→ProductVariant), user(BelongsTo→User)

### `stok_opnames` → StokOpname
`id` `created_by_id`(FK→users) `verified_by_id`(FK→users,null) `opname_date`(date) `status`(enum,default:draft) `notes`(text,null) `verification_notes`(text,null) `created_at` `updated_at` `deleted_at`
- Casts: status(StokOpnameStatus), opname_date(date:Y-m-d)
- Scopes: cancelled(), completed(), draft(), inProgress(), verified()
- Relations: createdBy(BelongsTo→User), verifiedBy(BelongsTo→User), stokOpnameItems(HasMany→StokOpnameItem)

### `stok_opname_items` → StokOpnameItem
`id` `stok_opname_id`(FK→stok_opnames) `product_variant_id`(FK→product_variants) `stock_quality`(enum,default:good) `system_stock`(uint,default:0) `physical_stock`(uint,default:0) `difference`(int,default:0) `notes`(text,null) `created_at` `updated_at`
- No soft deletes
- Casts: stock_quality(ProductStockQuality), system_stock(int), physical_stock(int), difference(int)
- Scopes: good(), reject()
- Relations: stokOpname(BelongsTo→StokOpname), productVariant(BelongsTo→ProductVariant)

### `retail_stock_histories` → RetailStockHistory
`id` `product_variant_id`(FK→product_variants) `user_id`(FK→users) `quantity`(uint) `stock_before`(uint) `retail_stock_before`(uint) `stock_after`(uint) `retail_stock_after`(uint) `notes`(255,null) `created_at`
- No soft deletes, no updated_at
- $timestamps = false
- Casts: quantity(int), stock_before(int), retail_stock_before(int), stock_after(int), retail_stock_after(int)
- Accessor: formatted_quantity → 'X.XXX', formatted_retail_stock_before → 'X.XXX', formatted_retail_stock_after → 'X.XXX', formatted_stock_before → 'X.XXX', formatted_stock_after → 'X.XXX'
- Relations: productVariant(BelongsTo→ProductVariant), user(BelongsTo→User)

### `stock_mutations` → StockMutation
`id` `user_id`(FK→users) `stockable_type`(string) `stockable_id`(ubig) `type`(string) `source_type`(string,null) `source_id`(ubig,null) `quantity`(int) `stock_before`(int) `stock_after`(int) `stock_quality`(string,null) `description`(string,null) `created_at` `updated_at`
- No soft deletes
- Casts: quantity(int), stock_before(int), stock_after(int)
- Accessor: formatted_quantity → 'X.XXX', formatted_stock_after → 'X.XXX', formatted_stock_before → 'X.XXX'
- Relations: stockable(MorphTo), source(MorphTo), user(BelongsTo→User)

---

## Settings

### `system_configurations` → SystemConfiguration
`id` `created_at` `updated_at`
- Singleton table

### `homepage_configurations` → HomepageConfiguration
`id` `created_at` `updated_at`
- Singleton table


### `settings` (laravel-settings)
`id` `group`(string) `name`(string) `locked`(bool,default:false) `payload`(json) `created_at` `updated_at`
- UNIQUE(group, name)

---

## Enums

| Enum | Values | Used In |
|------|--------|---------|
| `CashTransactionType` | deposit, expense, transfer, withdrawal | cash_transactions.type |
| `CuttingStatus` | in_progress, completed, cancelled | cuttings.status |
| `EmployeeAdvanceStatus` | pending, approved, rejected, paid, cancelled | employee_advances.status |
| `EmploymentStatus` | full_time, part_time, contract, internship, resigned | employees.employment_status |
| `Gender` | male, female | user_profiles.gender |
| `LeaveRequestStatus` | pending, approved, rejected, cancelled | leave_requests.status |
| `Modules` | — | Permission module names |
| `OrderChannel` | store, shopee, tiktok | orders.channel |
| `OrderStatus` | pending, processing, completed, cancelled, refunded | orders.status |
| `PaymentType` | cash, transfer, qris | orders.payment_type |
| `PayrollAdjustmentType` | bonus, deduction | payroll_adjustments.type |
| `PayrollPeriodStatus` | open, closed | payroll_periods.status |
| `PayrollStatus` | unpaid, paid, cancelled | payrolls.status |
| `Permission` | — | Permission action names |
| `PriceType` | retail, wholesale, capital | product_prices.type, orders.price_type |
| `ProductStatus` | active, draft, inactive, pending, rejected | products.status |
| `ProductStockQuality` | good, reject | order_items.stock_quality, restocks.stock_type, stok_opname_items.stock_quality |
| `RawMaterialUnit` | kg, meter, yard | raw_materials.unit |
| `Role` | — | Role names |
| `StokOpnameStatus` | draft, in_progress, completed, verified, cancelled | stok_opnames.status |

## Service Concerns

| Trait | Methods | Used By |
|-------|---------|---------|
| `HandlesCashTransactions` | getCashAccount(), creditCash(), debitCash() | CashAccountService, ExpenseService, EmployeeAdvanceService, PayrollPeriodService |
| `HasStockAdjustment` | adjustStock(), adjustVariantStock(), applyStock(), reverseStock() | TransactionService, RestockService |
| `RegistersMedia` | registerMedia(), syncPhoto() | CashAccountService, CuttingService, ExpenseService, PurchaseService, RestockService, TransactionService |

---

## Form Request Validation Rules

> Validasi harus SESUAI dengan DB schema (type + max length).

### Master

| Request | Field | Rules | DB Match |
|---------|-------|-------|----------|
| `CategoryRequest` | `name` | `required, string, max:50, unique:categories,name` | ✅ varchar(50) |
| `CustomerRequest` | `name` | `required, string, max:200, unique:customers,name` | ✅ varchar(200) |
| `CustomerRequest` | `phone_number` | `nullable, string, max:20` | ✅ varchar(20) |
| `CustomerRequest` | `address` | `nullable, string` | ✅ text |
| `SupplierRequest` | `name` | `required, string, max:200, unique:suppliers,name` | ✅ varchar(200) |
| `SupplierRequest` | `phone_number` | `nullable, string, max:20` | ✅ varchar(20) |
| `RawMaterialRequest` | `name` | `required, string, max:200` | ✅ varchar(200) |
| `RawMaterialRequest` | `unit` | `required, in:kg,meter,yard` | ✅ enum |
| `RawMaterialVariantRequest` | `variant` | `required, string, max:200` | ✅ varchar(200) |
| `RawMaterialVariantRequest` | `price` | `required, integer, min:0` | ✅ uint |
| `ProductRequest` | `name` | `required, string, max:200` | ✅ varchar(200) |
| `ProductVariantRequest` | `name` | `required, string, max:200` | ✅ varchar(200) |

### Finance

| Request | Field | Rules | DB Match |
|---------|-------|-------|----------|
| `CashAccountRequest` | `name` | `required, string, max:200, unique:cash_accounts,name` | ✅ varchar(200) |
| `CashTransactionRequest` | `amount` | `required, integer, min:1` | ✅ ubig |
| `CashTransactionRequest` | `description` | `required, string, max:100` | ✅ varchar(100) |
| `ExpenseRequest` | `amount` | `required, integer, min:1` | ✅ ubig |
| `ExpenseRequest` | `description` | `required, string, max:100` | ✅ varchar(100) |
| `EmployeeAdvanceRequest` | `amount` | `required, integer, min:1` | ✅ ubig |
| `EmployeeAdvanceRequest` | `description` | `required, string, max:100` | ✅ varchar(100) |
| `EmployeeAdvanceRequest` | `due_date` | `required, date, after_or_equal:today` | ✅ date |
| `EmployeeAdvancePaymentRequest` | `amount` | `required, integer, min:1` | ✅ ubig |
| `EmployeeAdvancePaymentRequest` | `description` | `nullable, string, max:100` | ✅ varchar(100) nullable |
| `PayrollAdjustmentRequest` | `type` | `required, in:bonus,deduction` | ✅ enum |
| `PayrollAdjustmentRequest` | `amount` | `required, integer, min:1` | ✅ ubig |
| `PayrollAdjustmentRequest` | `description` | `required, string, max:100` | ✅ varchar(100) |

### HR

| Request | Field | Rules | DB Match |
|---------|-------|-------|----------|
| `EmployeeRequest` | `email` | `required, email, max:100, unique:users,email` | ✅ varchar(100) |
| `EmployeeRequest` | `username` | `required, string, max:20, alpha_dash, unique:users,username` | ✅ varchar(20) |
| `EmployeeRequest` | `full_name` | `required, string, max:200` | ✅ varchar(200) |
| `EmployeeRequest` | `phone_number` | `nullable, string, max:20` | ✅ varchar(20) |
| `EmployeeRequest` | `gender` | `nullable, in:male,female` | ✅ enum |
| `EmployeeRequest` | `base_salary` | `required, integer, min:0` | ✅ uint |
| `AttendanceRequest` | `latitude` | `required, numeric, min:-90, max:90` | ✅ decimal(10,7) |
| `AttendanceRequest` | `longitude` | `required, numeric, min:-180, max:180` | ✅ decimal(10,7) |
| `LeaveRequestRequest` | `start_date` | `required, date, after:today` | ✅ date |
| `LeaveRequestRequest` | `end_date` | `required, date, after_or_equal:start_date` | ✅ date |

### Sales

| Request | Field | Rules | DB Match |
|---------|-------|-------|----------|
| `TransactionRequest` | `channel` | `required, in:store,shopee,tiktok` | ✅ enum |
| `TransactionRequest` | `price_type` | `required, in:retail,wholesale` | ✅ enum |
| `TransactionRequest` | `payment_type` | `required, in:cash,transfer,qris` | ✅ enum |
| `TransactionRequest` | `tiktok_order_id` | `nullable, string, max:100` | ✅ varchar(100) |
| `TransactionRequest` | `shopee_order_id` | `nullable, string, max:100` | ✅ varchar(100) |
| `TransactionRequest` | `notes` | `nullable, string, max:100` | ✅ text (UI limit) |

### Production

| Request | Field | Rules | DB Match |
|---------|-------|-------|----------|
| `CuttingRequest` | `description` | `nullable, string, max:100` | ✅ varchar(100) |
| `CuttingRequest` | `product_name` | `required, string, max:255` | ✅ varchar(255) |
| `CuttingRequest` | `cutting_result` | `required, integer, min:1` | ✅ uint |
| `CuttingRequest` | `sample` | `required, integer` | ✅ uint |
| `CuttingRequest` | `original_outside_sample` | `required, integer` | ✅ uint |

### Inventory

| Request | Field | Rules | DB Match |
|---------|-------|-------|----------|
| `PurchaseRequest` | `supplier_id` | `required, integer, exists:suppliers,id` | ✅ FK |
| `PurchaseRequest` | `notes` | `nullable, string, max:100` | ✅ varchar(100) |
| `RestockRequest` | `stock_type` | `required, in:good,reject` | ✅ enum |
| `RestockRequest` | `notes` | `nullable, string, max:100` | ✅ varchar(100) |

