# Queue API Go 1.26 modular-monolith API for the scenic queue system. It uses the standard library HTTP router, GORM, PostgreSQL, versioned embedded SQL migrations and database-backed sessions. ## Local run 1. Start PostgreSQL and create the database in `DATABASE_URL`. 2. Copy `.env.example` to `.env`, replace both cryptographic keys, and export the variables. Keys must decode to exactly 32 random bytes. 3. Run `go run ./cmd/api`. With `MIGRATE_ON_START=true`, embedded migrations run under a PostgreSQL advisory lock. 4. Seed the local demo users, projects, queues, batches, devices and audit data with: ```sh ADMIN_PASSWORD='replace-me' go run ./cmd/seed ``` The command is repeatable and prints the demo display and visitor paths. It resets only the `DEMO`, `RAFT` and `CABLE` projects. Default users are the protected `xqkwljtadmin` account and `staff`; override the staff credential and primary-project values with `ADMIN_USERNAME`, `STAFF_USERNAME`, `ADMIN_PASSWORD`, `PROJECT_CODE` and `PROJECT_NAME`. For a production release, run `go run ./cmd/migrate` as a separate release Job with `MIGRATE_ON_START=false`, then rotate the protected administrator password with `SUPER_ADMIN_PASSWORD='...' go run ./cmd/bootstrap-admin`. Do not run the seed command against production. `POST /api/staff/projects/{id}/tickets` and `POST /api/staff/projects/{id}/call-next` require an `Idempotency-Key` header. Ticket creation requires an immutable integer `party_size` inside the project's configured `min_party_size` / `max_party_size` range. A call-next request uses `{ "expected_revision": 12, "mode": "TICKET|PEOPLE", "count": 5 }`. `TICKET` selects up to `count` consecutive FIFO tickets. `PEOPLE` selects the longest consecutive FIFO prefix whose total party size does not exceed `count`; it never splits or skips a ticket and rejects when the first ticket alone is larger than the requested target. Each mode has a separate project-level anti-mistouch maximum. If a phone already has waiting tickets in the current queue session, ticket creation returns `DUPLICATE_PHONE`; repeat with the same request body except `allow_duplicate: true` and a new idempotency key after the employee confirms. Once the earlier ticket has been called, the same phone can take a new ticket without duplicate confirmation. `GET /api/public/projects` returns projects currently open for visitor self-service, including each project's allowed party-size range. `POST /api/public/projects/{id}/tickets` uses the same ticket validation, queue locking and idempotency rules as the staff ticket flow, but returns only the public ticket projection and a private status token. Public ticket creation is rate-limited per project and per normalized phone HMAC, and audited as `PUBLIC_TICKET_CREATED`. The default limits are 1000 project requests and 5 requests for the same project/phone in one minute; configure them with `PUBLIC_TICKET_PROJECT_LIMIT`, `PUBLIC_TICKET_PHONE_LIMIT` and `PUBLIC_TICKET_RATE_WINDOW`. Ticket creation does not reject by client IP. `POST /api/public/status/search` is a temporary non-production operational-test endpoint. It accepts `{ "phone": "..." }` and returns all current active tickets associated with that phone. It is disabled when `APP_ENV=production`; replace it with OTP or an external identity interface before formal launch. `POST /api/internal/status/search` exposes the same active-ticket lookup to trusted backend services in every environment. It accepts `{ "phone": "..." }` and only allows direct peers on private or loopback networks. Do not publish this path through the public Ingress; callers must use the internal service address. Forwarded client-IP headers are intentionally ignored. ## Tests ```sh go test ./... go vet ./... ``` From the repository root, `make test-db` creates a disposable PostgreSQL database and runs migration/maintenance integration tests. Unit tests cover FIFO ticket/people selection, people-ahead ETA, phone normalization, authenticated encryption, token hashing and password hashing. PostgreSQL integration tests run through `make test-db` against a disposable database.