# Migration Risks — ERP Gudang → Go/PostgreSQL Risk levels: **HIGH** (data/financial correctness), **MED** (behavioral divergence, manageable), **LOW** (cosmetic/dead code). --- ## R1. Non-atomic document numbering (HIGH) - Laravel generates `SO/PO/INV/PAY/CF/GR/SM/OPN/TRF/PV/CUS/SUP/PV/EMP` numbers via `Model::count()+1` in `creating` hooks. Two concurrent requests → duplicate numbers → unique-constraint violation or duplicate docs. - **Mitigation (approved):** PostgreSQL per-day sequences used inside the same transaction as the document insert. Number format strings stay identical (e.g. `SO-20260810-0001`). ## R2. Negative stock is possible (HIGH) - `SalesController::processPos` and the shared `adjustStock` never check `quantity >= requested` and do not lock. Confirmed in data: product 4 has `quantity = -1.0000`. - **Decision (approved): preserve Laravel behavior** for now (POS may go negative); flag for hardening. Manual stock-out already rejects insufficient stock. Go implementation should still: - add `SELECT ... FOR UPDATE` on the stock row in `adjustStock` (prevents lost updates / double-sell races), - make negativity a config flag (`ALLOW_NEGATIVE_STOCK`) defaulting to the Laravel behavior, documented in `migration-plan.md`. ## R3. Goods Receipt ledger integrity (repaired; historical gap remains) - The former raw stock increment bypassed `stock_movements`; goods receipts now lock stock and write mandatory source-linked `stock_in` movements in the receipt transaction. Movement failures roll back the receipt and balance. - Existing balances are retained. Historical missing movements require an evidence-backed reconciliation and are never synthesized by migration. `LOG_GR_MOVEMENTS` is removed; disabling audit history is not supported. ## R4. `products.current_stock` is dead (MED) - Column exists (int, default 0) but no code path ever writes it. All rows are 0. - **Decision:** keep the column in PG schema for schema-parity; do not use it. Note in docs. Do not use `products.current_stock` in Go queries (Laravel KPIs use `stocks` table). ## R5. `payDebt` Cashflow reference bug (MED) - `payDebt` never sets `reference_type`/`reference_id` on the Cashflow, and passes `reference_number`, which is not a fillable column → silently dropped. Consequently `exportSupplierDebt` "last payment" lookup is always empty. - **Decision (approved): preserve behavior** for parity. Flagged; recommend fix (set `reference_type=App\Models\PurchaseOrder`, `reference_id=po.id`, `category=purchase`) in hardening with client approval. ## R6. Money representation & rounding (HIGH) - All money `decimal(15,2)`; quantities `decimal(12,4)`. Laravel decimal casts return strings; `round()` used on PO tax (integer rounding, half-up); SO line totals are **unrounded** in PHP but truncated to 2dp by MySQL. - **Go must:** - use `shopspring/decimal` (or pgtype Numeric) — **never float64 for money**, - replicate `round(subtotal * tax_pct / 100)` integer half-up exactly, - replicate unrounded SO line totals stored to 2dp, - expose money as JSON **numbers** or strings consistently (decide in api-design.md; Vue `formatRupiah` uses `Number()` so numbers are safe). ## R7. Concurrency / lost updates (HIGH) - POS stock deduction, GR stock increment, and reserved-stock reservation are not atomic in Laravel. - **Go:** use `SELECT ... FOR UPDATE` + `pgx` transactions for all stock mutations, invoice/payment creation, and document-number allocation. Unique constraints preserved. ## R8. Authorization parity (MED) - Web enforces roles; **API routes enforce only `auth:sanctum`** (no role checks). If Go simply mirrors the API, mobile clients gain full access — unacceptable for the stated security requirement. - **Decision (approved):** Go enforces the full role/permission matrix on `/api/v1` endpoints (admin bypass preserved, `config/erp.php` permission globs + per-user `permissions` JSON override). ## R9. Auth token strategy (MED) - Laravel Sanctum: random 40-char token, SHA-256 hash stored in `personal_access_tokens`, bearer header. - **Go:** replicate opaque tokens (parity with existing data + instant revocation). Redis used only for rate limiting and optional token blacklist cache — **never as source of truth**. `expires_at`/`last_used_at` preserved. ## R10. MySQL → PostgreSQL type mapping (MED) - `ENUM` → `TEXT` + `CHECK`; `TINYINT(1)` → `BOOLEAN`; `TIMESTAMP` → `TIMESTAMPTZ`; generated column `stock_opname_items.difference` → PG `GENERATED ALWAYS AS ... STORED`. - Watch: MySQL `YEAR(4)` on payrolls.`period_year` → PG `SMALLINT`. - `sales_order_items.quantity` is `decimal(12,2)` in dump (not 12,4) — preserve exact precision. - Collation/index semantics differ; document-load script must assert row counts + money SUMs (parity checksums). ## R11. Data-load fidelity (HIGH) - `erp-gudang.sql` must load with **exact IDs preserved** (FK references across 36 tables). Load order must respect FKs. The dump may contain partial dumps of later tables. - **Mitigation:** idempotent, FK-safe, dry-run + full-import checksum report (row counts per table, SUM of monetary columns) compared against the dump. ## R12. Number/date/timezone semantics (MED) - Laravel `now()` uses `APP_TIMEZONE=Asia/Jakarta`. `date('Ymd')` in document numbers is server-local. - **Go:** use `time.Local` = Asia/Jakarta for all document-number date parts and "today" semantics; store UTC in DB where appropriate. Must match Laravel's local-date behavior for daily sequences. ## R13. Missing/dead flows (MED) - Stock transfer `processTransfer` method missing → behavior UNKNOWN; keep CRUD + draft only, document. - No delivery/pick flow for SO (`reserved_quantity` is never converted to a deduction on confirm/deliver). Preserve as-is; document. - `VoucherController` and `ProfileController` are redundant/shadowed — implement voucher under purchasing+finance, profile under auth. - `StockRequest.php` duplicate class — ignore. ## R14. Reporting query performance (LOW→MED at scale) - Laravel KPI queries are simple aggregates. At scale, add indexes: - `sales_orders (order_date)`, `purchase_orders (order_date)`, - `stock_movements (created_at)`, - `invoices (status, due_date)` (exists partially), - `cashflows (transaction_date)` (exists). - Consider a materialized view for the dashboard only if measurements demand it. **No premature analytics DB.** ## R15. Object storage (LOW, future) - Today product images/attendance photos go to local disk (`FILESYSTEM_DISK=local`). - **Go:** storage abstraction (interface) with a local-filesystem adapter now; S3/R2 adapter behind `STORAGE_*` env vars later. Do not hardcode any provider. ## R16. Background jobs (LOW) - Laravel has none (all synchronous). Go adds **no** async processing now; keep everything synchronous in transactions. Redis/Asynq reserved for future (PDF generation, notifications) only if required. ## R17. Idempotency for financial ops (MED) - Invoice creation and payment are the highest-risk duplicate points (client double-tap). - **Go:** add optional `Idempotency-Key` header → Redis-based dedup for `POST /payments`, `POST /invoices`, `POST /pos`. Preserve core behavior; the Redis dedup is additive and must not change results. ## R18. Sensitive data handling (MED) - `.env` contains `APP_KEY`; no secrets should be committed. Go `.env.example` only; `JWT_SECRET` unused (opaque tokens) — keep placeholder out of committed files. - Never log passwords/tokens/payment data. Structured logs carry request ID + user ID only. --- ## Cumulative approval gates (per migration policy) | Gate | Trigger | Status | |---|---|---| | Non-destructive schema | PG schema is new (no existing data destroyed) | n/a — new DB | | Data load | Requires checksum validation before swap | PENDING | | Destroy Laravel | Not until parity + explicit approval | NOT AUTHORIZED | | Negative-stock policy | Default = Laravel behavior (allowed via POS) | PENDING hardening | | GR ledger fix | Mandatory atomic source-linked movements; historical gaps not fabricated | IMPLEMENTED | | payDebt reference fix | Default = preserve bug | PENDING | | API role enforcement | **Enabled by default** (approved deviation) | APPROVED |