Files
baya-monorepo/dev/post-phase/refinement/RUNBOOK.md
T
2026-07-28 22:25:15 +03:30

250 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Run the whole app locally — Balinyaar bring-up runbook
The one copy-pasteable procedure for standing up **both** projects on one machine and watching a real,
authenticated request cross the wire. Established by **Refinement Phase 0**
([refinement-phase-0-bring-up.md](refinement-phase-0-bring-up.md)).
After this you have: the API on `https://localhost:5002` (against a local SQL Server), the web client on
`http://localhost:3000`, and a working phone-OTP login backed by the real backend. **Auth is the only real
domain** until [Refinement Phase 4](refinement-phase-4-frontend-de-mock.md) — everything else in the UI is
still an in-browser mock.
---
## Prerequisites
- **.NET 10 SDK** (`dotnet --version`) — preview is fine.
- **Node.js 20+** and **npm** (`node --version`).
- **Docker** (Desktop or engine) for the local database — or your own SQL Server on `localhost:1433`.
---
## One-time setup
### 1. Trust the ASP.NET Core HTTPS dev certificate
Without this the browser (and `fetch`) rejects `https://localhost:5002` and every call fails with an opaque
network error.
```bash
dotnet dev-certs https --trust
```
Accept the OS prompt. (macOS/Windows trust the cert; on Linux see the .NET docs for the per-distro step.)
### 2. Start a local SQL Server
From `server/`:
```bash
cd server
docker compose up -d
```
This runs SQL Server 2022 (Developer edition) on `localhost:1433` with a **dev-only** SA password
(`Balinyaar_Dev1433`, defined in `server/docker-compose.yml` — not a secret, never used in production).
Give it ~2030s on first start (`docker compose ps` shows `healthy`).
> Already have a SQL Server? Skip this and point the connection string in step 3 at it instead.
### 3. Point the API at the local database (via user-secrets — never a committed file)
The committed `appsettings*.json` carry a **placeholder** connection string on purpose. Supply the real
local one through `dotnet user-secrets` so no working credential ever lands in git. From the API project:
```bash
cd server/src/API/Baya.Web.Api
dotnet user-secrets set "ConnectionStrings:SqlServer" "Server=localhost,1433;Database=Baya;User Id=sa;Password=Balinyaar_Dev1433;TrustServerCertificate=True;Encrypt=False;"
```
The `Password` must match `MSSQL_SA_PASSWORD` in `docker-compose.yml`. User-secrets auto-load only in the
Development environment, so this never affects a deployed build.
> **Env-var alternative** (e.g. for CI/containers): set `ConnectionStrings__SqlServer` (double underscore =
> the `:` config separator) instead of using user-secrets.
> PowerShell: `$env:ConnectionStrings__SqlServer = "Server=localhost,1433;Database=Baya;User Id=sa;Password=Balinyaar_Dev1433;TrustServerCertificate=True;Encrypt=False;"`
> bash: `export ConnectionStrings__SqlServer="Server=localhost,1433;Database=Baya;User Id=sa;Password=Balinyaar_Dev1433;TrustServerCertificate=True;Encrypt=False;"`
---
## Run it (two terminals)
### Terminal 1 — backend (from `server/`)
```bash
dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj
```
On boot the API applies all EF migrations and seeds roles against the (empty) local DB, then listens on
**`https://localhost:5002`** — Swagger at `https://localhost:5002/swagger`. In **Development** it also seeds a
sandbox payment gateway and the demo world (below). It does **not** seed the old `admin`/`qw123321` account
anymore (refinement-phase-5); a break-glass admin is created only if you set `Seed:AdminUsername` /
`Seed:AdminPassword` (see below), and the day-to-day admin path is the phone-OTP demo admins.
In **Development** it additionally runs the **demo-world seeder** (Refinement Phase 1): verified/unverified
demo nurses with priced variants + Tehran coverage (and therefore real `nurse_search_index` rows), plus demo
customers with patients and addresses. The console logs `Demo world seeded: 3 nurse(s), 2 customer(s)…` (or
`already seeded — no-op` on a subsequent run). It is **idempotent** and **never runs outside Development**.
On top of that, the **lifecycle seeder** (`DemoLifecycleSeeder`) populates every flow's mid-state so manual
testing never starts from an empty world: booking requests in every status, 8 bookings across every reachable
state (incl. a 5-session package mid-engagement and a cancelled-and-refunded one), the balanced ledger, three
refunds (card-succeeded / BNPL-processing / post-payout clawback), a **paid** and a **draft** payout batch,
reviews in all moderation states, tickets, notifications, patient care records, a merchant-of-record partner
center, and a mid-pipeline verification case. The console logs `Demo lifecycle seeded: …` (or `already
seeded — no-op`). The full scenario map + per-flow walkthroughs live in
[`dev/post-phase/manual-testing-plan.md`](../manual-testing-plan.md).
### Terminal 2 — frontend (from `client/`)
```bash
cd client
npm install # first time only
npm run dev
```
Serves **`http://localhost:3000`**. It reads the API base URL from `client/.env.development`
(`NEXT_PUBLIC_API_URL = https://localhost:5002`). If you run the API on a different port/scheme, create
`client/.env.local` with your `NEXT_PUBLIC_API_URL=…` (it overrides `.env.development`, is git-ignored).
---
## Demo accounts (Development seed)
The demo seeder creates these loginable accounts (phone-OTP; any 6-digit code you read from the console/dev
endpoint works). Use them to see the real path populated:
| Phone | Role | Who | State |
| --- | --- | --- | --- |
| `09120000001` | nurse | زهرا عزیزی (female) | **verified**, 3 variants, whole-city + 2 districts |
| `09120000002` | nurse | علی کریمی (male) | **verified**, 2 variants, 3 districts |
| `09120000003` | nurse | مریم احمدی (female) | **unverified** — not discoverable in search |
| `09120000010` | customer | سارا محمدی (female) | 2 patients, 1 Tehran address |
| `09120000011` | customer | رضا حسینی (male) | 1 patient, 1 Tehran address |
| `09120000020` | admin (`super_admin`) | نگار مدیری (female) | full backoffice — **lands on `/admin`**, sees every console incl. RBAC |
| `09120000021` | admin (`finance`) | کامران مالی (male) | scoped backoffice — lands on `/admin`, sidebar shows only the money consoles (`useAdminCapabilities` gating) |
| `09120000030` | partner-center owner | بهنام رستگار (male) | owns مرکز پرستاری آرامش (merchant-of-record, sponsors علی کریمی). No admin/nurse role — log in, then navigate to `/partner` manually (REQ-038: no `/me` partner signal yet) |
> The old username+password `admin`/`qw123321` account is **no longer auto-seeded** (refinement-phase-5 —
> no committed credential). To bootstrap a break-glass username+password admin, set both secrets before boot,
> then log in via the API (not the web UI, which is phone-OTP only):
> ```bash
> cd server/src/API/Baya.Web.Api
> dotnet user-secrets set "Seed:AdminUsername" "admin"
> dotnet user-secrets set "Seed:AdminPassword" "<a-strong-password>"
> ```
The **phone-OTP admins** (`09120000020` / `09120000021`, refinement-phase-2) are how you reach the `/admin`
console through the same web login flow as everyone else — admin sub-roles are server-granted, never
self-selectable via `me/select_role`. Log in with either phone exactly like a nurse/customer; role
hydration routes you to `/admin`. To reach the **nurse** app, log in as a verified nurse phone
(`09120000001`); a fresh customer can also become a nurse in-app (SelectRole → `me/select_role`) and is
then routed to `/nurse` after the next `/me`.
Prove search works without the frontend: open Swagger →
`GET /api/v1/search/nurses?service_category_id=1&city_id=101` returns the two verified nurses' variants;
`service_category_id=3` (only the unverified nurse) returns an empty page.
## Log in (the real round-trip)
1. Open **`http://localhost:3000/fa/login`**.
2. Enter an Iranian mobile number (e.g. the verified nurse `09120000001`, or any demo phone above) and request
the code.
3. Get the 6-digit OTP one of two ways:
- **Read the server console** (Terminal 1) — SMS is mocked, so the code is logged:
`MOCK SMS — OTP code 123456 for phone ending in 0001`.
- **Or hit the Development-only helper** (handy for scripts/e2e):
`GET https://localhost:5002/api/v1/dev/last_otp/09120000001`
`{ "data": { "phone": "09120000001", "code": "123456" }, ... }`.
This endpoint returns **404 outside Development** and is superseded by real SMS in
[Refinement Phase 8](refinement-phase-8-external-rails.md).
4. Enter the code and submit → role hydration routes you to the app for your role: a customer to the family
home (`/`), a nurse to `/nurse`, an admin to `/admin` (refinement-phase-2).
5. **Verify in DevTools → Network:** `POST /api/v1/auth/request_otp`, `POST /api/v1/auth/verify_otp`, and
`GET /api/v1/me` all return **200** with the `ApiResult` envelope, and there is **no CORS error** in the
console. That is the first real authenticated request between the two projects.
### OTP over Telegram (optional — instead of reading the log)
For manual testing you can have the code arrive **on your phone in Telegram** rather than in the server
console. A standalone dev-only relay ([`telegram-otp-bot/`](../../../telegram-otp-bot/README.md)) forwards it;
the API talks to it through the normal `ISmsSender` seam. **Development only** — the relay *broadcasts* every
code to every configured chat id, so it is a test-group convenience, not an SMS gateway.
1. **Start the relay** (see its README for creating the bot with @BotFather and discovering chat ids —
each recipient must press **Start** in Telegram first, then `GET /chat_ids`):
```bash
cd telegram-otp-bot && npm start # no npm install — zero dependencies
```
`api.telegram.org` is filtered in Iran, so set `TELEGRAM_PROXY_URL` in its `.env` to your VPN/proxy
client (`http://127.0.0.1:10809`, `socks5://…`, or the proxy container on a VPS). The boot banner prints
the bot's `@username` — that line appearing means the token *and* the proxy work.
2. **Share the secret with the API** — the same value on both sides (relay `.env` `API_KEY`, API user-secret):
```bash
cd server/src/API/Baya.Web.Api
dotnet user-secrets set "Seams:Sms:Telegram:ApiKey" "<the relay's API_KEY>"
```
3. **Flip the provider** in `server/src/API/Baya.Web.Api/appsettings.Development.json`:
```jsonc
"Seams": { "Sms": { "Provider": "telegram" } } // committed default is "mock"
```
4. **Log in** as usual — the 6-digit code arrives in Telegram. The `dev/last_otp` helper keeps working
alongside it (`telegram` is the one non-mock provider that leaves the capture bridge on), so scripts and
e2e tests are unaffected. Set the provider back to `mock` to return to reading the console.
If the relay is down or reaches nobody it answers `502` and **login fails loudly** (`request_otp` returns an
error) rather than pretending an undelivered code was sent.
---
## Good to know
- **The API speaks HTTP/1.1 and HTTP/2** (Kestrel `Protocols: Http1AndHttp2`, refinement-phase-5 — the
previous HTTP/2-only default broke non-TLS HTTP/1.1 hops). Over TLS the client negotiates h2 via ALPN, so
gRPC and `fetch` both work; plain-HTTP hops fall back to HTTP/1.1.
- **Secrets fail fast.** On a fresh clone with no user-secrets the API refuses to start with
`Refusing to start: required secret configuration is missing…` — set the connection-string user-secret
(step 3) and boot again. Deployed environments must additionally supply real `IdentitySettings` JWE keys
and `Seams:FieldEncryption` keys (Development uses dev-only defaults from `appsettings.Development.json`).
- **Enable the secret-scan pre-commit hook** once per clone so a stray credential can't be committed:
`git config core.hooksPath .githooks` (see [`.githooks/README.md`](../../../.githooks/README.md)).
- **Behind a reverse proxy**, list its address in `ForwardedHeaders:KnownProxies` (or a CIDR in
`:KnownNetworks`) so the rate limiter partitions on the real client IP, not the proxy's.
- **Only `auth` is real by default.** 21 of 22 client service domains default to an in-browser mock
(`USE_*_MOCK = true`); the home, search, bookings, etc. are fake in-memory data until Refinement Phase 4.
- **The DB self-migrates + self-seeds**, so pointing at an empty local instance is enough — including the
Development demo world (nurses, variants, search rows, customers) from
[Refinement Phase 1](refinement-phase-1-database-and-seed.md).
- **Explicit migration path (deploy / migrations-off-boot).** Boot-time `MigrateAsync` is the dev convenience;
to apply migrations without booting the app (the direction [Refinement Phase 7](refinement-phase-7-unattended-ops.md)
will formalise), run from `server/`:
```bash
dotnet ef database update --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Api
```
(`dotnet tool install --global dotnet-ef` if the `ef` command is missing. It reads the same connection
string, so set the user-secret / env var first.) This applies **schema only** — the reference + demo seeds
run on the next app boot.
- **Allowed browser origins** are configuration-driven (`Cors:AllowedOrigins`), defaulting to
`http://localhost:3000` in Development. A deployed environment lists its real web origin(s).
## Stopping / resetting
```bash
docker compose down # stop the DB, keep its data
docker compose down -v # stop the DB and wipe the volume (fresh migrate + reference + demo seed next run)
```
Re-running `dotnet run` against an **existing** seeded DB does not duplicate anything — both the reference and
the demo seeders are idempotent (the demo seeder guards each persona on its phone number). To get a clean demo
world, wipe the volume (`docker compose down -v`) and boot again.
## Troubleshooting
| Symptom | Fix |
| --- | --- |
| Browser: `net::ERR_CERT_AUTHORITY_INVALID` on `:5002` | Run `dotnet dev-certs https --trust` (setup step 1). |
| API startup: `Refusing to start: required secret configuration is missing…` | The connection-string user-secret isn't set (or still the placeholder). Do setup step 3. |
| API startup: `Login failed for user 'sa'` / connect timeout | DB not up or wrong password — check `docker compose ps` and that the user-secrets password matches `docker-compose.yml`. |
| Console: `...has been blocked by CORS policy` | `UseCors` missing/mis-ordered, or the browser origin isn't in `Cors:AllowedOrigins`. It must sit after `UseRouting` and before the rate limiter. |
| `dotnet user-secrets` errors with "could not find UserSecretsId" | Run it from `server/src/API/Baya.Web.Api` (the project with `<UserSecretsId>`). |