Skip to main content

maintenance-service · LOCAL_DEV_SETUP

Goal: a fresh checkout boots the entire local stack with make local-up in under 5 minutes on macOS, Linux, or Windows (WSL2). All cloud dependencies have local emulators or first-class fakes.

1. Prerequisites

ToolVersionNotes
Node.js≥ 20.11 LTSMatch Cloud Run runtime
pnpm≥ 8.15Workspace package manager
Docker≥ 24For Postgres, Redis, Pub/Sub emulator, mocks
Docker Composev2bundled with recent Docker Desktop
GNU Make≥ 4.3bundled on macOS/Linux; via choco/scoop on Windows
gcloud CLIlatestOnly needed for staging deploys; not for local dev
Java JRE≥ 17Required by Pub/Sub emulator

Optional:

  • direnv for .envrc autoloading
  • mkcert for local HTTPS (we ship plain HTTP for dev)
  • psql 16 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

ContainerPortPurpose
postgres-165432Primary DB
redis-76379Memorystore-compatible
pubsub-emulator8085GCP Pub/Sub emulator
mock-ai-orchestrator4001Returns canned severity-suggestion and category-classify responses
mock-notification4002Captures outbound notifications; exposes GET /messages for assertions
mock-iam4003Issues JWTs for canonical test users; serves JWKS
mock-sync4004Echoes pull/push for desktop developers
mock-property4005Acknowledges room_blocked.v1 and emits the corresponding room.taken_out_of_order.v1
mock-reservation4006Returns canned overlapping reservation projection
mock-billing4007Acknowledges vendor.invoice_recorded.v1
outbox-relayn/aReads 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.v1 returns {"severity":"high","confidence":0.74,"rationale":"…"} for any input by default.
  • POST /capabilities/maintenance.category-classify.v1 returns {"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 /messages returns the list; DELETE /messages clears.
  • Useful in integration tests to assert what was sent.

mock-iam (port 4003)

  • Serves JWKS at /.well-known/jwks.json.
  • POST /tokens?role=staff_supervisor returns 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.v1 for the same room within 200 ms.
  • Toggle via env MOCK_PROPERTY_REJECT_BLOCKS=true to 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: 1 header for relocation testing.

mock-billing (port 4007)

  • Subscribes to melmastoon.maintenance.vendor.invoice_recorded.v1; auto-publishes melmastoon.billing.vendor_invoice.posted.v1 after 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

SymptomLikely causeFix
connection refused 5432 after make local-upPostgres not yet readywait 5 s; make logs SERVICE=postgres-16 to confirm
Pub/Sub publish hangsPUBSUB_EMULATOR_HOST unset; SDK trying real GCPsource .env.local or restart shell
Tests fail with JWKS_FETCH_ERRORmock-iam not runningmake local-up
outbox.lag_seconds risingoutbox-relay container crashedmake logs SERVICE=outbox-relay; restart
BFF says "WO 404" but DB has the rowwrong X-Tenant-Id header (RLS hid it)re-issue token with correct tenant
make migrate fails on CONCURRENTLYlocal Postgres is in a broken transaction statemake local-clean && make local-up
Docker Desktop CPU at 100%Pub/Sub emulator can be heavyreduce 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.sqltools with 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 dev boots the Electron renderer pointed at mock-sync on 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.