# Domain Map — ERP Gudang Maps the Laravel app into business domains for the Go modular monolith. Scope decision: **all domains in the Laravel app are migrated** (ERP core + HR + Sales Tracking + Vouchers). Legend: T = tables, M = models, C = controllers, R = web routes, A = API routes, V = validation requests, P = permissions. --- ## D1. Authentication - T: `users`, `personal_access_tokens`, `sessions` - C: `Auth\AuthController` (web), closures in `routes/api.php` (API) - R: `POST /login`, `POST /logout`, `GET|PUT /profile`, `PUT /profile/password` - A: `POST /api/login`, `GET /api/me`, `POST /api/logout` - Rules: - Email + password. Inactive users blocked (`is_active=false`). - Web: `session()->regenerate()` on login; `last_login_at` updated ≤1×/hour via `ActiveUserMiddleware`. - API: Sanctum token; `POST /api/login` returns token; logout deletes `currentAccessToken()`. - Passwords bcrypt (default rounds 12), cast `hashed`. - P: none (all users). ## D2. Users / Roles / Permissions - T: `users`, `roles`, `personal_access_tokens` - C: `SystemController` (users CRUD, roles CRUD, activity logs) - R: `/system/users*`, `/system/roles*`, `/system/activity-logs*` - Rules: - Roles: `admin|sales|finance|warehouse|hr_manager|hr_staff|employee|system` (string on `users.role`). - `roles` table is decorative — authz uses `users.role` + `config/erp.php role_permissions` + per-user `permissions` JSON override. - `User::can(permission)`: admin has `*`; role glob match (e.g. `masterdata.*`); user JSON override. - `RoleMiddleware`: admin bypasses all role lists. - P: system (admin only), users (admin only). ## D3. Master Data — Products / Categories / Units / Suppliers / Customers / Warehouses - T: `products`, `categories`, `units`, `suppliers`, `customers`, `warehouses` - M: Product (SoftDeletes, auto code), Category, Unit, Supplier (SoftDeletes, auto code, `getTotalDebtAttribute`), Customer (SoftDeletes, auto code, `assigned_sales_id`), Warehouse (`manager_id`) - C: `MasterDataController` - R: `/master-data/{products,categories,units,suppliers,customers}` CRUD + `GET /master-data/products/search` - Rules: - Product code auto-generated: `-YYYYMMDD-` where prefix = first 3 uppercase letters of category name, default `PRD`; sequence = count of products in that category (incl. trashed) + 1; uniqueness loop. - Customer code: `CUS-`. Supplier code: `SUP-`. - Product image upload ≤2MB, stored on local disk. - Soft deletes on products/suppliers/customers; category/unit deletion restricted while products reference them (`restrictOnDelete`). - Supplier `total_debt` = sum of `remaining_amount` of POs with status `received|partial`. - P: masterdata for `admin,warehouse,sales` (web route: `admin,sales`); read access broader. ## D4. Inventory — Stocks / Movements / Opname / Transfer - T: `stocks`, `stock_movements`, `stock_opnames`, `stock_opname_items`, `stock_transfers`, `stock_transfer_items` - M: Stock (`getAvailableQuantityAttribute` = quantity − reserved), StockMovement (ledger, before/after), StockOpname (+items), StockTransfer (+items) - C: `InventoryController` - R: `/inventory/stock`, `/inventory/movements`, `/inventory/opname*`, `/inventory/transfer*` - Rules: - Stock row unique per `(product_id, warehouse_id)`. - `adjustStock(product, warehouse, delta, type)` (shared static, also used by Sales/POS): - firstOrCreate stock row → `before = quantity` → `after = before + delta` → update row → create StockMovement `SM-YYYYMMDD-`, `quantity=abs(delta)`. - **No row lock inside adjustStock** (only manual stock-out locks first). - Manual stock-out: locks row, rejects if `quantity < requested`. - Default warehouse: first active warehouse, fallback `1`. - Opname: snapshot all stock rows as items (`system_quantity`), finalize applies `difference` per item via `adjustStock(diff, 'opname')`, `status: draft|in_progress|completed|cancelled`. - Transfer: `TRF-YYYYMMDD-`, statuses `draft|approved|in_transit|completed|cancelled`; receiving/processing logic incomplete in Laravel. - P: inventory for `admin,warehouse`. ## D5. Sales / POS - T: `sales_orders`, `sales_order_items` - M: SalesOrder (auto `SO-YYYYMMDD-`), SalesOrderItem - C: `SalesController` - R: `/sales/orders*`, `/sales/pos*`, `/sales/pos/process`, `/sales/receivables*` - Rules: - SO statuses: `draft|confirmed|picking|shipped|delivered|invoiced|paid|cancelled`. Order types: `regular|pos|consignment`. - Regular SO: draft → items (discount % per line) → reserve stock (increment `reserved_quantity`) → confirm → **no stock deduction on confirm** (missing delivery flow in Laravel). - Edit allowed only while `draft` (release old reserved, re-reserve, recompute). Cancel releases reserved. Delete only while `draft`. - POS: create SO `confirmed` → line items → `adjustStock(-qty, 'stock_out')` per line → auto-invoice (+7d, unpaid) → SO `invoiced`. **No stock availability check** (negative stock possible). - Headers store computed `subtotal, tax_amount, discount_amount, total_amount, paid_amount, remaining_amount, shipping_cost`. - P: sales for `admin,sales,finance`; `sales` role sees only own orders. ## D6. Purchasing / Goods Receipt - T: `purchase_orders`, `purchase_order_items`, `goods_receipts`, `goods_receipt_items` - M: PurchaseOrder (auto `PO-YYYYMMDD-`, morph payments), PurchaseOrderItem, GoodsReceipt (auto `GR-YYYYMMDD-`), GoodsReceiptItem (batch/expiry) - C: `PurchasingController` - R: `/purchasing/po*`, `/purchasing/goods-receipts*`, `/purchasing/debt*`, `/purchasing/vouchers*` - Rules: - PO statuses: `draft|approved|sent|partial|received|paid|cancelled`. - PO create: line `sub = max(0, qty×price − discount)`; `tax = round(subtotal × tax_pct/100)` (**integer rounding**); `total = max(0, subtotal + tax − discount_amount)`. - GR: per line update stock via raw SQL `quantity + received`; **no StockMovement, no lock**; PO → `received` if fully received else `partial`. - Debt: PO `remaining_amount`; pay via `payDebt` (amount ≤ remaining) → updates PO, optional voucher (`available→used`), creates Cashflow expense (category `sales`). **Reference fields on Cashflow not set** (parity bug, see migration-risks.md). - P: purchasing for `admin,warehouse,finance`. ## D7. Finance — Invoices / Payments / Cashflow / Receivables / Payables - T: `invoices`, `payments`, `cashflows`, `vouchers` - M: Invoice (auto `INV-YYYYMMDD-`, `isOverdue` = due_date0), Payment (morph `payable`, auto `PAY-YYYYMMDD-`), Cashflow (auto `CF-YYYYMMDD-`), Voucher (auto `PV-YYYYMMDD-`) - C: `FakturisasiController` (invoices/payments), `FinanceController` (cashflow), `PurchasingController` (payDebt, vouchers), `VoucherController` - R: `/fakturisasi/invoices*`, `/finance/cashflow*`, `/purchasing/vouchers*` - Rules: - Invoice create from SO in `confirmed|delivered`; total = SO total; due = +7 days; SO → `invoiced`. Invoice `subtotal/tax_amount` left 0. - Invoice pay: amount >0 and ≤ remaining; updates invoice; creates Payment (morph=Invoice) + Cashflow income (category `sales`, sets reference correctly). - Cashflow types `income|expense`; categories `sales|purchase|salary|operational|other`. - P: fakturisasi `admin,sales,finance`; finance `admin,finance`. ## D8. Reports / Dashboard - T: none (computed) - C: `DashboardController`, plus list/summary methods across controllers - R: `/dashboard`, `/dashboard/kpis`, `/launch` - Rules: - KPIs: month sales total, month purchase total, receivables balance, payables balance, low-stock count, recent activity. - Low stock: product active AND (any stock row `quantity <= products.min_stock` OR no stock row at all). - Cashflow summary: `SUM(amount)` grouped by type within date filter. - Exports: CSV for receivables, supplier debt, cashflow. - P: dashboard for all authenticated users; role-specific module map. ## D9. HR — Employees / Attendance / Leaves / Payroll / Performance - T: `employees`, `attendances`, `leaves`, `payrolls`, `performances` - M: Employee (SoftDeletes, auto `EMP-`), Attendance, Leave, Payroll, Performance - C: `HRController` - R: `/hr/employees*`, `/hr/attendance*`, `/hr/leaves*`, `/hr/payroll*`, `/hr/performance*` - A: `POST /api/hr/clock-in`, `POST /api/hr/clock-out`, `POST /api/hr/leaves`, `GET /api/hr/my-attendance`, `GET /api/hr/my-payslips`, `GET /api/hr/my-leaves` - Rules: - Work hours 08:00–17:00, late tolerance 15 min, overtime threshold 8h (config). - Attendance status `present|late` (+ absent/leave/holiday/wfh enum). - Payroll: hard-coded rates BPJS-TK emp 2.7% (config 2.0%), employer 3.7%; BPJS-KES emp 1%, employer 4%; overtime 1.5×; net = basic+allowances+overtime+bonus−deductions−bpjs−pph21. `status: draft|approved|paid`. - P: hr for `admin,hr_manager,hr_staff,employee`; employee CRUD `admin,hr_manager`; delete employee admin-only. ## D10. Sales Tracking — Visits / Activities / Routes / Locations - T: `sales_visits`, `sales_activities`, `sales_routes`, `sales_locations` - M: SalesVisit, SalesActivity, SalesRoute, SalesLocation - C: `SalesTrackingController` - R: `/sales-tracking/visits*`, `/activities*`, `/routes*` - A: `POST /api/tracking/location`, `POST /api/tracking/visits/{id}/checkin|checkout`, `GET /api/tracking/my-visits` - Rules: - Visit statuses `planned|in_progress|completed|cancelled`; GPS lat/lng; duration = check_out − check_in. - Location ping interval 60s; high-frequency endpoint excluded from activity logging. - Route `planned_customers` JSON `[{customer_id, order, estimated_time}]`. - P: sales_tracking for `admin,sales`. ## D11. System / Audit - T: `activity_logs` - M: ActivityLog (record helper) - C: `SystemController` - R: `/system/activity-logs` - Rules: - ActivityLogMiddleware logs all successful (status<400) POST/PUT/PATCH/DELETE for authenticated users, skipping `dashboard` and `api/tracking/location`. - Manual `ActivityLog::record()` calls inside most controller mutations. - P: system (admin). --- ## Cross-domain notes - **Employee ↔ User:** `employees.user_id` (1:1) — used to scope "my" data on mobile (attendance, payslips, leaves, visits). - **Warehouse default:** used by Sales, POS, and Inventory (first active warehouse, fallback 1). - **Stock ledger:** single `stock_movements` table referenced by Sales (stock_out) and Inventory (stock_in/out/opname); Purchase GR path bypasses it. - **Money:** every monetary column `decimal(15,2)`; quantities `decimal(12,4)`.