143 lines
6.9 KiB
Markdown
143 lines
6.9 KiB
Markdown
# Fiesta backend (`nearle`)
|
||
|
||
The Go/Fiber API behind the Nearle Daily merchant console, the customer app,
|
||
the rider app and the in-store POS terminals. Postgres (`nearledb`) for
|
||
tenants, stores, products, stock and orders; a separate pgvector database for
|
||
the global product catalogue; Redis for POS presence; MQTT for the tills.
|
||
|
||
This page is the map. Each section says what a thing is, how to use it, and
|
||
where the detail lives.
|
||
|
||
## Run it
|
||
|
||
```sh
|
||
export PATH="$PATH:$HOME/go/bin" # Go 1.24 lives there on the dev Macs
|
||
docker compose -f docker-compose.local.yml up -d # postgres :5433, pgvector :5434, redis :6379
|
||
go run . # APP_ENV unset → .env.local, listens on :1122
|
||
go test ./...
|
||
```
|
||
|
||
An empty database is not enough — startup runs migrations that assume the
|
||
live schema. `init/README.md` explains loading a schema dump first.
|
||
|
||
Startup prints what it loaded and where it is pointed:
|
||
|
||
```
|
||
config: loaded .env.local
|
||
config: APP_ENV=local, listening on :1122, database nearle@localhost:5433/nearledb
|
||
scan: product search uses openai/all-minilm # or: EMBEDDING_PROVIDER not set, text-only
|
||
```
|
||
|
||
## Configuration
|
||
|
||
Everything comes from environment variables, read once by `config.Load()`.
|
||
|
||
| You want to… | Do this |
|
||
|---|---|
|
||
| Run locally | Nothing — `.env.local` is loaded by default |
|
||
| Run against production settings | `APP_ENV=production go run .` (⚠ every write is real) |
|
||
| See every variable and what it does | `.env.example` |
|
||
| Add a new setting | Add it to `config.Config` + `Load()` + `.env.example`, **and to the cluster** (`nearle-config` ConfigMap or `app-secrets` Secret in namespace `nearle`) — a variable in the file and not in the cluster is unset in production |
|
||
| Find out why it won't boot | Read the message — it lists *every* missing variable at once |
|
||
|
||
Precedence is `real environment > .env.<APP_ENV> > .env`. The container gets
|
||
no `.env` file at all (`.dockerignore`); the `Dockerfile` sets
|
||
`APP_ENV=production` and the values come from Kubernetes.
|
||
|
||
Full detail, including the committed-credentials situation:
|
||
**`docs/ENVIRONMENT.md`**.
|
||
|
||
## Layout
|
||
|
||
```
|
||
main.go boot: config → databases → migrations → routes → MQTT → listen
|
||
config/ env-file loading, typed Config, validation
|
||
db/ Postgres (nearledb + catalogue), Redis, S3 image store
|
||
facade/ wires repositories → services → controllers (add new modules here)
|
||
routes/ one file per module; /live/api/v1/web/... (console) and /v1/mob/... (apps)
|
||
controllers/ HTTP in, HTTP out — parse, call the service, shape the envelope
|
||
services/ the rules; no SQL, no HTTP
|
||
repositories/ the SQL; nothing else
|
||
models/ request/response and table shapes
|
||
messaging/ MQTT ingest from tills, console live stream
|
||
utils/ small shared helpers (tokens, geo, embeddings, geocoding)
|
||
docs/ integration specs for the frontends and handoff notes
|
||
scratch/ one-off read/verify tools run with `go run ./scratch/<name>`
|
||
init/ schema/seed for the local database
|
||
```
|
||
|
||
Every response uses the same envelope:
|
||
`{ "code": 200, "status": true, "message": "…", "details": … }`.
|
||
Business outcomes ("out of stock", "not registered") are 200s with a reason
|
||
in the body; HTTP errors mean the request could not be served at all.
|
||
|
||
## Adding an endpoint
|
||
|
||
1. **Model** the request/response in `models/`.
|
||
2. **Repository** method(s) in `repositories/` — SQL only, take a
|
||
`context.Context`, return `error`.
|
||
3. **Service** in `services/` — the rules, with sentinel errors
|
||
(`ErrXxxBadRequest`, `ErrXxxNotFound`) the controller can map to statuses.
|
||
4. **Controller** in `controllers/` — `BodyParser`/`Query`, call the service,
|
||
map sentinel errors to 400/404/503, everything else to 500.
|
||
5. **Routes** file in `routes/`, registered in `routes/routes.go`.
|
||
6. **Wire** it in `facade/container.go`.
|
||
7. **Test** the service with a fake repository (see `services/scan_test.go`
|
||
for the pattern) — no database needed.
|
||
|
||
`services/scanService.go` + `controllers/scanController.go` are a complete,
|
||
current example of all seven.
|
||
|
||
## Features with their own docs
|
||
|
||
| Feature | For | Doc |
|
||
|---|---|---|
|
||
| Environment & deployment | everyone | `docs/ENVIRONMENT.md` |
|
||
| Scan-to-order (camera → product → nearest store with stock) | mobile app | `docs/SCAN_TO_ORDER.md` |
|
||
| Catalogue import into a store | console | `docs/CATALOGUE_IMPORT_INTEGRATION.md` |
|
||
| POS terminal ingest, login, API | POS / tills | `docs/POS_*.md` |
|
||
| Access-control audit and what is still open | everyone | `docs/SECURITY_HANDOFF.md` |
|
||
|
||
## Things to know before you get surprised
|
||
|
||
- **There is no auth layer.** `customerid` / `tenantid` in a request are
|
||
trusted. `docs/SECURITY_HANDOFF.md` §1 is the standing issue.
|
||
- **Login errors mean what they say.** `409 Invalid Email` = the query ran
|
||
and matched nobody. `500 Login is temporarily unavailable` = the database
|
||
could not answer (it used to be reported as Invalid Email; see
|
||
`services/userService.go` `lookupLogin`).
|
||
- **Stock is a ledger.** Live stock is always `SUM(in) − SUM(out)` of
|
||
`productstocks` at an outlet, never a stored number. Filter on the same
|
||
expression you display (`services/productVisibility.go` explains why).
|
||
- **The catalogue is a different database** and must never be reached
|
||
through the `nearledb` handle. Its per-brand tables are discovered from
|
||
`information_schema`; brands appear and columns vary.
|
||
- **Catalogue ids are not stable** across re-scrapes; `imageid` is the
|
||
durable key (`models.Products.Imageid`).
|
||
- **Migrations run on boot** and are guarded by `IF NOT EXISTS` / schema
|
||
checks, not a version table. Read the comments in `main.go` before adding
|
||
one — several have bitten before.
|
||
- **One MQTT client id per replica.** A second connection with the same id
|
||
evicts the first. Never run a local process with the production
|
||
`MQTT_URL`.
|
||
- **`scratch/` tools read production** when run with `.env.production`, and
|
||
most are read-only. Two are not — `termbackfill` and
|
||
`cataloguefactsbackfill` repair rows that no endpoint can reach. Both
|
||
default to a dry run that prints every change and write only when passed
|
||
`apply`, and both print the SQL to undo themselves afterwards. A new tool
|
||
that writes follows that shape or it does not write.
|
||
|
||
## Operations cheat-sheet (Kubernetes, namespace `nearle`)
|
||
|
||
```sh
|
||
kubectl get pods -n nearle # fiesta-0/1/2 (StatefulSet)
|
||
kubectl logs -n nearle fiesta-0 | grep -E 'config:|scan:|pos:'
|
||
kubectl exec -n nearle fiesta-0 -- env | grep EMBEDDING_
|
||
kubectl set env statefulset/fiesta -n nearle KEY=value # adds a var and rolls the pods
|
||
kubectl rollout status statefulset/fiesta -n nearle
|
||
```
|
||
|
||
The embedding model behind scan-to-order is served by the cluster's Ollama
|
||
(`ollama.krow.svc.cluster.local:11434`); `docs/SCAN_TO_ORDER.md` has the
|
||
exact settings and why that model.
|