maintenance-service · LOCAL_DEV_SETUP
Goal: a fresh checkout boots the entire local stack with
make local-upin under 5 minutes on macOS, Linux, or Windows (WSL2). All cloud dependencies have local emulators or first-class fakes.
1. Prerequisites
| Tool | Version | Notes |
|---|---|---|
| Node.js | ≥ 20.11 LTS | Match Cloud Run runtime |
| pnpm | ≥ 8.15 | Workspace package manager |
| Docker | ≥ 24 | For Postgres, Redis, Pub/Sub emulator, mocks |
| Docker Compose | v2 | bundled with recent Docker Desktop |
| GNU Make | ≥ 4.3 | bundled on macOS/Linux; via choco/scoop on Windows |
| gcloud CLI | latest | Only needed for staging deploys; not for local dev |
| Java JRE | ≥ 17 | Required by Pub/Sub emulator |
Optional:
direnvfor.envrcautoloadingmkcertfor local HTTPS (we ship plain HTTP for dev)psql16 for ad-hoc DB inspection
2. One-shot bootstrap
git clone git@github.com:melmastoon/platform.git
cd platform/services/maintenance-service
pnpm install # workspace install at repo root works too
cp .env.example .env.local
make local-up # brings up the stack
make migrate # applies db/migrations
make seed # inserts canonical test tenants + properties + assets
pnpm dev # starts NestJS in watch mode (port 3030)
Health check:
curl http://localhost:3030/healthz
# {"status":"ok","build":"local","time":"..."}
3. make local-up brings up
| Container | Port | Purpose |
|---|---|---|
postgres-16 | 5432 | Primary DB |
redis-7 | 6379 | Memorystore-compatible |
pubsub-emulator | 8085 | GCP Pub/Sub emulator |
mock-ai-orchestrator | 4001 | Returns canned severity-suggestion and category-classify responses |
mock-notification | 4002 | Captures outbound notifications; exposes GET /messages for assertions |
mock-iam | 4003 | Issues JWTs for canonical test users; serves JWKS |
mock-sync | 4004 | Echoes pull/push for desktop developers |
mock-property | 4005 | Acknowledges room_blocked.v1 and emits the corresponding room.taken_out_of_order.v1 |
mock-reservation | 4006 | Returns canned overlapping reservation projection |
mock-billing | 4007 | Acknowledges vendor.invoice_recorded.v1 |
outbox-relay | n/a | Reads our outbox, publishes to emulator |
Compose file: docker/local/docker-compose.yml. Logs: make logs SERVICE=<name>.
4. Required environment variables
.env.local (auto-created from .env.example):
NODE_ENV=local
PORT=3030
LOG_LEVEL=debug
BUILD_VERSION=local
# DB
DB_HOST=localhost
DB_PORT=5432
DB_NAME=maintenance_local
DB_USER=maintenance
DB_PASSWORD=maintenance
DB_SSL=disable
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
# Pub/Sub emulator
PUBSUB_PROJECT_ID=melmastoon-local
PUBSUB_EMULATOR_HOST=localhost:8085
# JWT / IAM
IAM_URL=http://localhost:4003
JWT_JWKS_URL=http://localhost:4003/.well-known/jwks.json
JWT_ISSUER=http://localhost:4003
# Service URLs (mocks)
AI_ORCHESTRATOR_URL=http://localhost:4001
NOTIFICATION_URL=http://localhost:4002
SYNC_URL=http://localhost:4004
PROPERTY_URL=http://localhost:4005
RESERVATION_PROJECTION_URL=http://localhost:4006
BILLING_URL=http://localhost:4007
# Tenant fixtures
DEFAULT_DEV_TENANT=tnt_local_dev_001
DEFAULT_DEV_PROPERTY=prop_local_kabul_1
DEFAULT_DEV_USER=usr_local_supervisor_001
# Workers
WORKER_BATCH_SIZE_PREVENTIVE=200
WORKER_BATCH_SIZE_SLA=500
VENDOR_REMINDER_DEFAULT_MINUTES=30
Production-only secrets are not required locally; their absence triggers the local mock paths.
5. Seed data (make seed)
The seeder creates a deterministic baseline:
- Tenants:
tnt_local_dev_001(Kabul),tnt_local_dev_002(Peshawar) — second tenant exists to verify isolation in manual testing. - Properties:
prop_local_kabul_1(12 rooms),prop_local_kabul_2(8 rooms),prop_local_peshawar_1(10 rooms). - Users: owner, GM, two supervisors, four technicians per tenant.
- Vendors:
vnd_local_habibi_electrical(channel: whatsapp, phone+93700123456, no email)vnd_local_kabul_plumbing(channel: sms)vnd_local_acme_hvac(channel: email)
- Assets per property: 1 generator (Cummins 25 kVA), 2 HVAC units, 1 main water tank, 6 lock devices (mirrored externalRef from mock-lock).
- Parts per property: dust filter, capacitor, hose clamp, lock battery (CR123A).
- Preventive schedules:
- generator: composite 250 h / 90 d
- HVAC dust filter: 30 d
- water tank: 90 d
- lock battery check: 30 d
- Open WOs (3 per property, mixed severity) so the BFF has something to render immediately.
Seed is idempotent: make seed resets to baseline by default; pass make seed APPEND=1 to add without dropping.
6. Common dev commands
pnpm dev # NestJS watch, port 3030
pnpm test # all tests (unit + integration; will start Testcontainers)
pnpm test:unit # unit only (fast, no docker)
pnpm test:int # integration only
pnpm test:contract # Pact + JSON Schema golden
pnpm lint # ESLint + tsc --noEmit
pnpm build # tsc -p tsconfig.build.json
make migrate # node-pg-migrate up
make migrate-down # node-pg-migrate down (one step)
make migrate-create NAME=add_foo
make openapi # regenerate openapi/v1.yaml from NestJS decorators
make events-schema # regenerate JSON Schemas from TS event interfaces
make seed # reset + seed canonical data
make logs SERVICE=postgres-16
make local-down # tear down stack (volumes preserved)
make local-clean # tear down + drop volumes
7. Mock service behaviours (cheat sheet)
mock-ai-orchestrator (port 4001)
POST /capabilities/maintenance.severity-suggestion.v1returns{"severity":"high","confidence":0.74,"rationale":"…"}for any input by default.POST /capabilities/maintenance.category-classify.v1returns{"category":"hvac","confidence":0.92}by default.- Override via
X-Mock-Response: <preset>header. Presets:low_severity,low_confidence_category,timeout,5xx,budget_exhausted.
mock-notification (port 4002)
- Stores every outbound message in an in-memory list.
GET /messagesreturns the list;DELETE /messagesclears.- Useful in integration tests to assert what was sent.
mock-iam (port 4003)
- Serves JWKS at
/.well-known/jwks.json. POST /tokens?role=staff_supervisorreturns a JWT for the canonical test user.- Roles configurable via query.
mock-property (port 4005)
- Subscribes to
melmastoon.maintenance.work_order.room_blocked.v1(via Pub/Sub emulator). - Auto-publishes
melmastoon.property.room.taken_out_of_order.v1for the same room within 200 ms. - Toggle via env
MOCK_PROPERTY_REJECT_BLOCKS=trueto test the rejection path.
mock-reservation (port 4006)
GET /projection/reservations/active-overlapping?roomId=&from=&to=returns[]by default.- Override via
X-Mock-Overlap-Count: 1header for relocation testing.
mock-billing (port 4007)
- Subscribes to
melmastoon.maintenance.vendor.invoice_recorded.v1; auto-publishesmelmastoon.billing.vendor_invoice.posted.v1after 1 s.
8. Issuing dev tokens manually
TOKEN=$(curl -sX POST 'http://localhost:4003/tokens?role=staff_supervisor&tenant=tnt_local_dev_001&user=usr_local_supervisor_001' | jq -r .token)
curl http://localhost:3030/api/v1/maintenance/work-orders \
-H "Authorization: Bearer $TOKEN" \
-H "X-Tenant-Id: tnt_local_dev_001" \
-H "X-Property-Id: prop_local_kabul_1"
9. Running just the workers locally
pnpm dev:workers # boots Cloud-Scheduler-equivalent local ticker that hits the worker entrypoints every 60s
Workers respect the same .env.local. Cron timings are accelerated in local mode (every 10 s) for faster feedback; toggle with WORKER_FAST_TICK=false to test real cadence.
10. Resetting between test scenarios
make reset # drops + recreates DB, reseeds, clears emulator topics, clears mock state
make reset-quick # only clears domain tables; keeps schema and seed structure
11. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
connection refused 5432 after make local-up | Postgres not yet ready | wait 5 s; make logs SERVICE=postgres-16 to confirm |
| Pub/Sub publish hangs | PUBSUB_EMULATOR_HOST unset; SDK trying real GCP | source .env.local or restart shell |
Tests fail with JWKS_FETCH_ERROR | mock-iam not running | make local-up |
outbox.lag_seconds rising | outbox-relay container crashed | make logs SERVICE=outbox-relay; restart |
| BFF says "WO 404" but DB has the row | wrong X-Tenant-Id header (RLS hid it) | re-issue token with correct tenant |
make migrate fails on CONCURRENTLY | local Postgres is in a broken transaction state | make local-clean && make local-up |
| Docker Desktop CPU at 100% | Pub/Sub emulator can be heavy | reduce by stopping unused mocks; we keep emulator-only profile via make local-up PROFILE=core |
12. IDE configuration
- VS Code workspace settings ship in
.vscode/:- ESLint + Prettier on save
- Recommended extensions:
dbaeumer.vscode-eslint,esbenp.prettier-vscode,ms-azuretools.vscode-docker,mtxr.sqltoolswith the Postgres driver. - Tasks for
pnpm dev,make migrate,pnpm test.
- A
.devcontainer/is provided for VS Code Remote Containers users.
13. Working on the desktop sync side
If you also work on the Electron desktop app:
cd ../../desktop && pnpm devboots the Electron renderer pointed atmock-syncon port 4004.- Toggle "offline mode" in the dev menu to verify queueing behavior; commands accumulate locally and fire once you flip back online.
- Conflict UI tested by changing the same WO's status both in the Electron app and via REST while offline.