# ERP Gudang — Backend Migration (Laravel → Go) Migrating the business logic of the existing Laravel ERP (reference: `C:\Project\Website\ERPGUDANG`) into an API-first Go backend serving ERP Web, POS Web, E-Commerce/B2B, Android, and iOS. - Target: **Go + PostgreSQL + Redis + REST (`/api/v1`) + OpenAPI + Docker** - Architecture: **modular monolith** (see `docs/architecture.md`) - The Laravel app remains the reference system and is **not modified**. ## Migration status ``` AUDIT / DOCS [DONE] ARCHITECTURE [DONE] OPENAPI [DONE] GO FOUNDATION [DONE] AUTH [DONE] MASTER DATA [DONE] INVENTORY [DONE] SALES / POS [DONE] PURCHASE [DONE] FINANCE [DONE] REPORTS [DONE] PARITY TESTS [DONE] SCHEMA MIGRATIONS [DONE] HARDENING [DONE] (security headers, body limit, KPI cache) HR [DONE] SALES TRACKING [DONE] SYSTEM / AUDIT [DONE] (users, roles, activity logs, audit middleware) DEPLOY [DONE] (Dockerfile, GitHub Actions CI, GHCR image, seed) PHASE 1 CONTRACT SYNC [DONE] (route/OpenAPI parity tests) PHASE 2 P0 INTEGRITY [DONE] (real PostgreSQL/Redis tests, idempotency) PHASE 3 MONTHLY NUMBERING [DONE] (atomic monthly document sequences) PHASE 4 DISTRIBUTION [DONE] (DO, partial shipment, fleet, dispatch) PHASE 5 SHIPMENT TRACKING [DONE] (tracking/barcode lookup, location events) PHASE 6 B2B / RETAIL [IN PROGRESS] item 1 catalog [DONE] (brands, product units, barcodes) item 2 pricing engine [DONE] (authoritative prices, line snapshots) item 3 membership [DONE] (tiers + identity split + tier history; benefit formulas CONFIRMED Bronze 2% / Gold 5% / Platinum 8% frozen into immutable snapshot) items 4-8 [PENDING] promotions/vouchers, B2B workflows, galon, returns, cashier shift — blocked on business confirmation PHASE 7 HARDENING/IMPORT [NOT STARTED] PRODUCTION CUTOVER [NOT AUTHORIZED] ``` Phase detail, dependency order, and the open business-confirmation gates live in `docs/migration-plan.md` §Phase 6; the authoritative rules are in `docs/business-rules.md`. ## Documentation | Doc | Purpose | |---|---| | `docs/laravel-audit.md` | Environment + architecture of the Laravel reference | | `docs/domain-map.md` | Business domains → tables/models/controllers/routes/permissions | | `docs/database-analysis.md` | MySQL schema → PostgreSQL mapping, data snapshot | | `docs/business-rules.md` | Authoritative business rules + UNKNOWN register | | `docs/route-map.md` | Existing routes → `/api/v1` candidates | | `docs/migration-risks.md` | Risk register + approval gates | | `docs/architecture.md` | Go modular-monolith design | | `docs/api-design.md` | API conventions (envelopes, errors, pagination) | | `docs/migration-plan.md` | Schema build, data load, phase sequencing | | `docs/sqlc-atlas-migration.md` | sqlc repository and Atlas migration workflow | | `docs/openapi.yaml` | API contract for the Vue frontend (Phase 3) | | `deploy/MIDTRANS-SWEEPER.md` | Sweeper setup + stranded-payment reconciliation runbook | ## Key decisions (approved) - **New PostgreSQL schema mirroring MySQL** + one-time checksum-verified data load from `erp-gudang.sql`. - **Atomic document numbering** via DB sequences (replaces Laravel `count()+1`). - **Opaque bearer tokens** (Sanctum-parity) with server-side revocation. - **Money = `NUMERIC(15,2)` + shopspring/decimal** — never float. - **Stock receipt integrity is mandatory**: goods receipts write source-linked stock movements atomically; no flag disables the ledger. Negative-stock and debt-reference compatibility remain configured separately. - **API enforces the full role/permission matrix** (closes the Laravel API authorization gap). ## Local development (Phase 4+) ```bash # run directly (needs PostgreSQL; Redis optional): psql -U postgres -c "CREATE ROLE erp LOGIN PASSWORD 'erp';" psql -U postgres -c "CREATE DATABASE erp OWNER erp;" atlas migrate apply --dir file://migrations \ --url "postgres://erp:erp@localhost:5432/erp?sslmode=disable" psql -U erp -d erp -f seed.sql DATABASE_URL=postgres://erp:erp@localhost:5432/erp?sslmode=disable go run ./cmd/server ``` Environment: copy `.env.example` → `.env`; never commit real secrets. Interactive API documentation (Scalar): `http://localhost:8080/docs` (raw contract at `/openapi.yaml`, source: `docs/openapi.yaml`). Default seed accounts (see `seed.sql`): | email | password | role | |---|---|---| | `admin@keluargamart.local` | `Admin@1234` | admin | | `buyer@keluargamart.local` | `Buyer@1234` | sales | ## Building the backend when debugging - **Linux** — let GitHub Actions build and publish the image, then run it with PostgreSQL and Redis connection URLs supplied through the environment: ```bash docker pull ghcr.io/tarulab/keluargamart-backend:latest docker run --rm -p 8080:8080 --env-file .env.prod \ ghcr.io/tarulab/keluargamart-backend:latest ``` - **Windows** — build and run locally without Docker (needs a local PostgreSQL; Redis is optional). Apply the Atlas migration directory and `seed.sql` first (see "Local development" above): ```powershell go build -o server.exe ./cmd/server $env:DATABASE_URL="postgres://erp:erp@localhost:5432/erp?sslmode=disable" .\server.exe ``` ## Deployment ### Full stack (docker compose) — recommended ```bash cp .env.example .env # set POSTGRES_PASSWORD; DATABASE_URL must match it docker compose up -d ``` Runs the backend, PostgreSQL, Redis, and all three frontends. The `db-seed` service initializes a fresh database by applying every file in `migrations/*.sql` in sequence, followed by `seed.sql` — it skips when the `users` table already exists. Frontends listen on `B2B_PORT` (3002), `ERP_PORT` (3001), and `POS_PORT` (3003); each proxies `/api` and `/uploads` to the backend. Use Atlas for schema upgrades on an existing database, not the seed service. ### Standalone container Hostnames in `.env.prod` must resolve from both the VPS shell and the container; compose-only names such as `postgres` and `redis` do not work here. ```bash cp .env.example .env.prod # set DATABASE_URL, REDIS_URL, CORS_ORIGINS set -a && . ./.env.prod && set +a atlas migrate apply --dir file://migrations --url "$DATABASE_URL" psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f seed.sql # fresh DB only docker pull ghcr.io/tarulab/keluargamart-backend:latest docker rm -f keluargamart-backend 2>/dev/null || true docker run -d --name keluargamart-backend --restart unless-stopped \ -p 8080:8080 --env-file .env.prod \ ghcr.io/tarulab/keluargamart-backend:latest ``` For a private GHCR package, authenticate first with a token that has `read:packages`: ```bash echo "$GHCR_PAT" | docker login ghcr.io -u --password-stdin ``` Pin a release tag such as `v1.2.3` instead of `latest` for reproducible deployments. Put TLS termination and request limits in a reverse proxy in front of port 8080. Do not run `seed.sql` as a routine upgrade: its account inserts are intended for initial provisioning only. ### Verification ```bash curl http://localhost:8080/health curl http://localhost:8080/ready curl -X POST http://localhost:8080/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"admin@keluargamart.local","password":"Admin@1234"}' ``` `/ready` reports degraded when Redis is unavailable, even if PostgreSQL is connected. This is expected for setups that intentionally omit Redis. ### Schema changes ```bash atlas migrate validate --dir file://migrations atlas migrate apply --dir file://migrations --url "$DATABASE_URL" ``` **Automatic migration on boot:** the backend embeds `migrations/*.sql` into its binary and applies pending revisions on startup (`internal/dbmigrate`, tracking table `schema_migrations`). A database that predates this runner — the `users` table exists but tracking is empty — is baselined: every revision is recorded without executing anything, so existing deployments are never mutated by an upgrade. Set `AUTO_MIGRATE=false` to disable and apply manually with Atlas as above. Startup fails fast (non-zero exit) if a migration errors. Do not edit migrations already applied to an environment. Add the next numbered forward migration, refresh `db/schema.sql`, run `sqlc generate`, and commit the query source together with generated `internal/db` output. ### Existing pre-Atlas database Use a baseline only when adopting Atlas for a database whose schema was already brought to the exact state represented by migrations through `000004`. A baseline records those migrations as applied without running them. It is not a way to repair a partial or drifted schema. First take a backup, restore it to a disposable database, and verify that the restored schema matches a fresh database built by this migration directory. Also confirm that Atlas has never managed the target: `atlas migrate status` should not report existing revisions. Then preview and deliberately confirm the one-time baseline: ```bash pg_dump "$DATABASE_URL" --format=custom --file=pre-atlas-backup.dump atlas migrate apply --dir file://migrations --url "$DATABASE_URL" \ --baseline 000004 --dry-run # Set this in the shell only after the backup and schema-equivalence checks # above pass. The command must fail closed when it is not explicitly set. : "${CONFIRM_ATLAS_BASELINE:?Set CONFIRM_ATLAS_BASELINE=existing-schema-matches-000004 after review}" test "$CONFIRM_ATLAS_BASELINE" = existing-schema-matches-000004 atlas migrate apply --dir file://migrations --url "$DATABASE_URL" \ --baseline 000004 atlas migrate status --dir file://migrations --url "$DATABASE_URL" ``` Do not use `--baseline` for a fresh database; apply all migrations normally. Do not baseline an existing database if the dry run proposes schema migrations or if a revision table already contains Atlas history. Stop and reconcile the schema and migration history instead. ## Continuous integration (GitHub Actions) ![CI](https://github.com/TaruLab/keluargamart-backend/actions/workflows/docker.yml/badge.svg) `.github/workflows/docker.yml` runs on every push and pull request: 1. **Test** — validate and apply Atlas migrations to fresh PostgreSQL, regenerate sqlc code and check tracked and untracked output for drift, then run `go build ./...`, `go vet ./...`, and `go test ./...`. 2. **Build image** — `linux/amd64` via Buildx with GHCR layer caching. PRs only build (verification); push happens on `main` and on `v*` tags. Published image (GHCR, public once the package is made public): ``` ghcr.io/tarulab/keluargamart-backend latest <- main branch sha- <- every main commit vX.Y.Z <- git tag ``` Deploy the CI-built image with the "Standalone container" steps under [Deployment](#deployment) above, or run the whole stack with `docker compose`. ## Repo layout ``` cmd/server/ entrypoint + wiring (main.go) internal/ auth, health, catalog, pricing, partners, warehouses, inventory, sales, purchasing, finance, distribution, reports, hr, salestracking, system, cart (+ parity, integration) pkg/ config, logger, httpresponse, decimal, database, redis, middleware db/ versioned sqlc schema snapshot + query sources migrations/ Atlas migration directory + checksum docs/ audit + design + API contract seed.sql bootstrap data (accounts, master data, membership tiers) seed_testdata.sql dev/QA demo data — kept in the seed path until launch ``` ## Resuming work 1. Read `docs/migration-plan.md` §7 for the work board and §Phase 6 for the dependency order of the remaining items. 2. Read `docs/business-rules.md` §20 (UNKNOWN register) — Phase 6 items 3-8 need business answers on membership formulas, promotion stacking, galon deposits, return policy, and cashier shift binding before they can be implemented. 3. Apply pending migrations before running anything: `atlas migrate apply --dir file://migrations --url "$DATABASE_URL"`. 4. Verification gates for any change: `gofmt -l` on changed files, `go build ./...`, `go vet ./...`, `go test -count=1 ./...` (needs `DATABASE_URL`; `REDIS_URL` enables the idempotency-backed POS routes), `atlas migrate validate`, and `sqlc generate` with no drift. 5. Schema changes follow `docs/sqlc-atlas-migration.md`: add the next numbered forward migration, refresh `db/schema.sql` via `atlas schema inspect`, run `sqlc generate`, then update models/repository/service/handlers/tests/OpenAPI together.