Files
baya-monorepo/dev/post-phase/refinement/RUNBOOK.md
T

256 lines
15 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 a database
**`dotnet user-secrets` is no longer used** — the `<UserSecretsId>` was removed from
`Baya.Web.Api.csproj`, so that store isn't read at all. A leftover `secrets.json` on your machine is inert
and can be deleted. All configuration lives in
[`appsettings.Development.json`](../../../server/src/API/Baya.Web.Api/appsettings.Development.json), which
already points at the **shared remote database** the deployed demo also uses — so a fresh clone boots with
no configuration step at all.
To work against the throwaway local container from step 2 instead, edit `ConnectionStrings:SqlServer` in
that file (the `Password` must match `MSSQL_SA_PASSWORD` in `server/docker-compose.yml`):
```jsonc
"SqlServer": "Server=localhost,1433;Database=Baya;User Id=sa;Password=Balinyaar_Dev1433;TrustServerCertificate=True;Encrypt=False;"
```
> **Env-var alternative** (CI/containers, or to avoid a local edit showing up in `git status`): set
> `ConnectionStrings__SqlServer` (double underscore = the `:` config separator) — it overrides the file.
> 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;"`
> Deploying rather than developing? See [DEPLOY.md](../../../DEPLOY.md).
---
## 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, add both keys to
> `appsettings.Development.json` before boot, then log in via the API (not the web UI, which is phone-OTP only):
> ```jsonc
> "Seed": { "AdminUsername": "admin", "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: the relay's `.env` `API_KEY` and
`Seams:Sms:Telegram:ApiKey` in `appsettings.Development.json`. The appsettings side is already filled
in; copy that value into your local `telegram-otp-bot/.env`. Do **not** use the one in `.env.example` —
it is published in git, so `TelegramSmsSender` rejects it with
`Seams:Sms:Telegram:ApiKey is not configured (unset, or still the published example 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.** If `ConnectionStrings` is blank or still the base file's
`SET_VIA_USER_SECRETS_OR_ENV` placeholder, the API refuses to start with
`Refusing to start: required secret configuration is missing…`. Production/Staging must additionally supply
real `IdentitySettings` JWE keys and `Seams:FieldEncryption` keys; Development uses the dev-only ones in
`appsettings.Development.json`.
- **Never change `Seams:FieldEncryption:Key`/`:HashKey`.** They decrypt every PII column in the shared
database and derive `users.PhoneHash`, which every login looks up. Changing either locks everyone out.
- **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…` | `ConnectionStrings:SqlServer` is blank or still a 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 password in `appsettings.Development.json` 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" | Expected — user-secrets was removed. Edit `appsettings.Development.json` instead. |