# Phase 2 — Integration & dependency

**Depends on:** Phase 0 · **Can run in parallel with:** Phase 1 · **Blocks:** Phase 3 · **Size:** one session

## Goal

Make the client↔server dependency a **thing you can read in one place**, and re-sync the contract
layer with the code it stopped tracking on 2026-07-13.

Right now the seam is real but scattered: envelope shape and casing in one contract doc, cookies and
refresh in `client/CLAUDE.md`, CORS in a refinement report, `NEXT_PUBLIC_API_URL` in `.env` files,
the container topology in `DEPLOY.md` and `docker-compose.yml`, and the OTP relay in its own README.
Nobody can answer "what does the client actually need from the server?" without reading six files.

---

## Inputs

- `docs/integration/openapi/swagger.v1.json` — the **fresh** snapshot from Phase 0, plus the
  added/removed endpoint diff Phase 0 wrote into `docs/_plan/open-contradictions.md`.
- [dev/contracts/](../../dev/contracts/) — 18 domain files + 2 convention files + the stale snapshot.
- [dev/shared-working-context/frontend/requests/for-backend.md](../../dev/shared-working-context/frontend/requests/for-backend.md) —
  67 REQs; many *are* contract amendments that were delivered but never folded back into the domain docs.
- [dev/shared-working-context/reports/mocks-registry.md](../../dev/shared-working-context/reports/mocks-registry.md) — which seams are mocked.
- [DEPLOY.md](../../DEPLOY.md) · [docker-compose.yml](../../docker-compose.yml) · [deploy/Caddyfile](../../deploy/Caddyfile)
- [telegram-otp-bot/README.md](../../telegram-otp-bot/README.md) + [INTEGRATION-PROMPT.md](../../telegram-otp-bot/INTEGRATION-PROMPT.md)
- `client/.env.development`, `client/.env.production`, `server/src/API/Baya.Web.Api/appsettings*.json`
- Code, for the seam itself: `client/src/lib/api/`, `client/src/services/*/`, the server's
  `ApiResult` envelope, CORS setup, and auth middleware.

## Outputs

```
docs/integration/
  index.md            THE seam, one page: what crosses the wire and what each side owes the other
  api-contract.md     envelope, casing, pagination, errors, idempotency, auth headers/cookies
  domains/            22 files, one per service domain — refreshed against the live swagger
  openapi/
    swagger.v1.json   (from Phase 0)
    README.md         how to regenerate, when it was last taken, endpoint count
  config-matrix.md    every env var / appsettings key, both projects + docker + the bot
  topology.md         the runtime dependency graph
```

Also updated: [DEPLOY.md](../../DEPLOY.md) — fix the stale `user-secrets` reference and link to
`docs/integration/topology.md`. It stays at root and stays the deploy *procedure*.

---

## Steps

### 1. Write `index.md` first — the seam on one page

Before touching the per-domain detail, answer these in one page:

- **Transport**: HTTP/JSON over `NEXT_PUBLIC_API_URL`; where gRPC exists and whether it is used.
- **The envelope**: `ApiResult` — `{ isSuccess, statusCode, message, requestId, data }`, payload
  always under `data`, `requestId` is a W3C trace id (refinement-phase-9). Client unwraps in
  `clientFetch`/`unwrap()`.
- **Casing**: JSON bodies camelCase; URL segments snake_case. (REQ-001 settled this — confirm against
  the live swagger, don't trust the doc.)
- **Pagination**: `{ items, total, page, pageSize }`; the server binds `pageSize` case-insensitively
  (REQ-010).
- **Errors**: status codes, machine-readable error codes (REQ-003), what the client does with 401 /
  403 / 5xx / network.
- **Auth**: the cookie set, JWE opacity to the client, silent refresh, session rotation and
  reuse-detection, `/me` and `me/select_role`, role hydration.
- **Idempotency**: which endpoints require `Idempotency-Key` and what the client generates.
- **Money**: IRR on the wire, Toman at the UI boundary — link to `docs/rules/shared/`.
- **What the server owes the client** and **what the client owes the server**, as two short lists.

### 2. Refresh the 22 domain contracts against the live swagger

For each domain, compare the contract doc against `swagger.v1.json` and the handler code. Mark each
endpoint: `matches` · `drifted (describe)` · `undocumented (in code, not in contract)` ·
`phantom (in contract, not in code)`.

Fold in the delivered REQs — 17+ were delivered in refinement-phase-3 and amended shapes that the
domain docs still describe the old way. **The domain doc is the thing that should be true; the REQ
ledger is a change log.** After this phase, a reader should never need to read the REQ file to know
the current shape.

Note the domain-file cleanup: there is both a `messaging.md` (851 B stub) and a
`messaging-notifications-admin.md` (10.6 KB) — merge. Align the file set with the client's 22
`services/` domains so the mapping is one-to-one where it can be.

Anything you cannot verify from swagger or code: mark `UNVERIFIED:` and add a row to Phase 4's input
list. Do not guess a shape.

### 3. Write `config-matrix.md`

One table: **key · where it's set (appsettings / .env / docker-compose / Caddyfile) · consumed by ·
required? · default · notes**. Cover at minimum:

- `NEXT_PUBLIC_API_URL`, `NEXT_PUBLIC_NESHAN_KEY`, and the rest of the client's `NEXT_PUBLIC_*`
- `IdentitySettings:SecretKey` / `:Encryptkey`
- `Seams:FieldEncryption:Key` / `:HashKey` — **flag as load-bearing and immutable**
- the connection strings (app DB + log DB), and that the DB is remote and **not** containerised
- `OpenTelemetry:Otlp:Endpoint`, the health endpoints (`/healthz/live`, `/healthz/ready`)
- the seam selectors that choose real vendor adapters vs mocks (refinement-phase-8)
- the Telegram OTP bot's token/chat config
- CORS origins, and the Caddy hostnames `balinyaar.ir` / `api.balinyaar.ir`

State plainly, once, that config lives in files and `dotnet user-secrets` is not used — and that this
is a deliberate pre-launch trade with live credentials in git (link to `DEPLOY.md` "Going to
Production" and to Phase 5's `pre-launch.md`).

### 4. Write `topology.md`

The runtime dependency graph — a mermaid diagram plus a short table:

```
browser -> Caddy (caddy_net) -> client container (Next.js)  -> server container (ASP.NET)
                              -> server container            -> remote SQL Server (not containerised)
                                                             -> telegram-otp-bot (OTP relay)
                                                             -> object storage / external rails (per seam config)
```

For each edge: what flows over it, what breaks if it is down, and where it is configured. This is the
"dependency between projects, documented in a proper place" deliverable.

### 5. Point the old locations at the new one

`dev/contracts/README.md` gets a one-line "moved to `docs/integration/`" banner (Phase 6 archives the
folder; until then, don't leave two live copies competing).

---

## Verification

- [ ] Every endpoint in `swagger.v1.json` appears in exactly one `docs/integration/domains/*.md`, or
      is listed in that file's "undocumented, intentionally" section with a reason.
- [ ] No `phantom` endpoints remain undeclared — each is either removed from the doc or listed as
      "planned, not built" with a backlog reference for Phase 4.
- [ ] `config-matrix.md` accounts for every key in `appsettings.Production.json`,
      `docker-compose.yml`, and both `.env` files — diff mechanically, don't eyeball.
- [ ] `DEPLOY.md` no longer instructs anyone to use `user-secrets`.
- [ ] `index.md` fits on one screen-and-a-half and answers the seam question without a single click.

## Definition of done

A frontend agent can build against the API by reading `docs/integration/index.md` plus one domain
file, and a deploy question is answered by `topology.md` + `config-matrix.md` without opening
`docker-compose.yml`.

## Handoff

> Run 2026-07-30 against commit `d3ec723`. **All verification items pass**, checked mechanically
> (script in the session scratchpad, results reproduced below).

### What shipped

```
docs/integration/
  index.md            153 lines — the whole seam, one screen and a half
  api-contract.md     envelope · casing · status codes · auth · pagination · idempotency · money · enums · rate limits
  config-matrix.md    every key in both appsettings, docker-compose, 3 .env files, the bot, Caddy, CORS
  topology.md         mermaid graph + a 10-edge table + ports + startup order + the local variant
  domains/
    index.md          the census, the route-shape exceptions, the enum map, what-replaced-what
    <22 files>        one per client services/ domain, 1:1
  openapi/README.md   updated: C-9 resolved, the servers-block provenance wrinkle recorded
```

Also updated: `DEPLOY.md` (links topology + config-matrix; states there is no `appsettings.Production.json`
so step 2 of "Going to Production" is a *create*), `docs/README.md` (integration → **written**),
`dev/contracts/README.md` + `openapi/README.md` + `domains/messaging.md` (moved/merged banners).

### Verification

| Item | Result |
| --- | --- |
| Every swagger path declared in exactly one `domains/*.md` | **178/178**, 0 unreferenced, 0 duplicated |
| Operation census adds up | 186 = 184 domain + 2 `ping` in `api-contract.md` |
| Every phantom declared with a reason + REQ | **24/24** |
| `config-matrix.md` accounts for every key | Diffed mechanically; 5 gaps found and written up |
| `DEPLOY.md` no longer instructs `user-secrets` | Pass — it already documented the removal; now also links the new files |
| `index.md` fits a screen and a half | 153 lines |
| Every doc carries a `Last verified` stamp | 27/27 |
| No reference file over 400 lines | Pass |
| Relative links resolve | **216 checked, 0 broken** |

### Scope note

`api-contract.md` was **merged into `index.md`'s siblings as planned**, but the plan's `domains/` count of
22 was met by re-cutting the file set along the **client's** domain boundaries rather than the backend
phases the old 17 files used. That is what makes the mapping 1:1 and is why the file names changed.

`swagger.v1.json` was **not** re-fetched — Phase 0's 2026-07-29 snapshot is 1 day older than this run and
the server was not booted. Everything here is derived from that snapshot plus current source.

---

### Drift list — Phase 4's input

**A. Phantom endpoints (24).** The client's real API clients call 24 routes the server does not expose.
Each is declared in its domain file with its REQ. Grouped by REQ:

| REQ | Status | Phantoms | Domain |
| --- | --- | --- | --- |
| REQ-031 | deferred | 3 (`admin_roles/*`) | admin |
| **REQ-061** | **never filed** | 2 (`admin_users/search`, `/lookup`) | admin |
| REQ-022 | partial | 3 (`checkout_bnpl/options`, `/schedule`, `/wallet_installments`) | bnpl |
| REQ-032 | partial | 5 (`centers/me/*`, `partner-centers/{id}/nurses`) | partner-center |
| REQ-033 | partial | 1 (`centers/me/settlement`) | partner-center |
| REQ-064 | open | 1 (`centers/me/bookings/{id}`) | partner-center |
| REQ-034 | deferred | 3 (`admin_verifications` documents-url / approve / reject) | verification |
| REQ-035 | deferred | 3 (`admin_refunds` preview / approve / reject) | refunds |
| REQ-036 | deferred | 1 (`admin_payouts/{id}/transfer_reference`) | payouts |
| REQ-047 | open | 1 (`bookings/payment_history`) | payment |
| REQ-048 | open | 1 (`refunds/my`) | refunds |
| REQ-063 | open | 1 (`tickets/{id}/assign`) | tickets |

**Two are live 404s** because their domain's mock is off: `bookings/payment_history` (payment) and
`tickets/{id}/assign` (tickets). Both are guarded in the client, so neither is user-visible — but they are
reachable, not hypothetical.

**B. Five REQs whose filed status is wrong or narrower than reality.** Each was checked against the
swagger, not against another document. Phase 4 should re-triage these rather than copy the ledger.

| REQ | Filed as | Actually |
| --- | --- | --- |
| REQ-050 | open — "the list DTO carries neither field" | `BookingRequestListItemDto` **has `variantLabel`** (and `patientAge`). It lacks `variantPrice`/`variantPriceUnit` and the `status=answered` group filter. The REQ was verified against the *client type*, not the wire |
| REQ-063 | open — "no close/reopen/assign routes yet" | `tickets/{id}/close` and `/reopen` **exist and are wired**. Only `assign` is missing. `tickets/constants.ts` carries the stale reason |
| REQ-066 | open — needs an anonymous search read | `GET /search/nurses` **is already anonymous**. What is missing is the rate limit — `SearchController` has no `[EnableRateLimiting]` |
| REQ-067 | open — needs an anonymous profile read | `GET /nurses/{id}/profile` **is already anonymous**. What is missing is the privacy review |
| REQ-029 / REQ-030 | delivered | Correct — but `admin/constants.ts` still names them as reasons the admin mock is primary. The only live reasons are REQ-031 and REQ-061 |

**C. Client-side drifts (client work, not contract work).**

| # | Where | What |
| --- | --- | --- |
| 1 | `client/src/lib/api/types.ts` | `ApiEnvelope<T>` omits **`code`**, which the server sends and `clientFetch` already reads at runtime. Incomplete type, no defect |
| 2 | `client/src/services/admin/apis/clientApi.ts` | `pageQuery()` sends **`page_size`**; these endpoints declare `PageSize`. Model binding is case-insensitive, **not separator-insensitive**, so it binds nothing and every admin list silently gets the default page size. Latent until `USE_ADMIN_MOCK` flips |
| 3 | `client/src/services/bookingRequests/types.ts` | Marks `variantLabel` as client-augmented/absent; the server serves it. Widening the type would light up the redesigned inbox card today |
| 4 | `client/src/services/payouts/apis/clientApi.ts` | Comment says `NursePayoutHistoryDto` has no `failureReason`; **the wire has it** on all three payout read models |
| 5 | `client/src/services/tickets/constants.ts` | Says the backend has no close/reopen/assign; close and reopen exist (B, above) |
| 6 | `client/src/services/payouts/apis/clientApi.ts` | Sends `Idempotency-Key` on `admin_payouts` process/retry, where the server never reads it. Decorative, not harmful |
| 7 | `client/.env.sample` | Still `NEXT_PUBLIC_API_URL = https://localhost:5002` — the `https` half of **C-3**, in the file a fresh clone copies. `.env.development` is correct |
| 8 | `client/src/services/profiles/` | `POST customer_profiles/avatar` exists and is live; the client only wires the nurse route, so a customer cannot set a photo |

**D. Cross-side vocabulary asymmetry (1).** The client's `TicketAuthorRole` includes **`system`**;
`Entities/Messaging/TicketCodes.cs` defines only `customer`/`nurse`/`admin`. Safe on the reading side, but a
reader of the client types would wrongly conclude the server emits it. All other 17 shared vocabularies
match exactly, both directions.

**E. Route-shape exceptions (4).** `admin/partner-centers` (+ `/set-active`, `/sponsor-nurse`),
`admin/tickets`, `admin/reviews/…` and `internal/bookings/…` hardcode route strings instead of using the
`[controller]`/`[action]` tokens; three introduce hyphens, against the snake_case rule. **Because the route
also derives the dynamic-permission key, normalising them breaks permissions as well as URLs** — recorded,
not fixed.

**F. Config gaps (5).** Written up in
[config-matrix.md § What the diff found](../integration/config-matrix.md#what-the-diff-found):
`NEXT_PUBLIC_EVV_MOCK_GPS` and `NEXT_PUBLIC_VERSION` read but declared in no `.env`;
`OpenTelemetry:Otlp:Endpoint`, `Search:Backend` and `Seed:Admin*` read but set nowhere (and a bad
`Search:Backend` **throws at startup**); `.env.sample`'s stale `https`; the Telegram shared secret
duplicated across two files with nothing keeping them equal; no `appsettings.Production.json`.

**G. Unfiled REQ (1).** `ui-phase-11-report.md` records "REQ-061…064 appended" to the ledger. **Only
062/063/064 were.** Ten live client files cite REQ-061 for the admin user directory
(`admin_users/search`, `admin_users/lookup`). Phase 4 must **file it**, not assume it is tracked.

### Contradictions resolved here

**C-6** · **C-7** · **C-8** · **C-9** closed; **C-2** closed as a documentation decision; **C-3**'s
contract-layer half closed. Decisions recorded in
[open-contradictions.md § Resolved](open-contradictions.md#resolved).

### What Phase 3 should know

- The seam facts it needs are settled and mechanically verified: camelCase bodies, snake_case URLs, bearer
  header (**not** cookie auth), money as a digit string outbound and an int64 inbound, `Idempotency-Key` on
  exactly two endpoints, 20 anonymous operations.
- **`GET /api/v1/dev/last_otp/{phone}` is live on `api.balinyaar.ir`** — the deployment runs as Development
  with a capture-safe SMS provider, so both gate conditions hold. Any flow doc that describes logging in
  should say this is how you do it today, and that it is the deployment's largest exposure.
- C-4 (the `dev-certs` step) is confirmed dead: the API binds plain HTTP locally, so there is no certificate
  to trust. C-5's two bring-up worlds are real and give **different data** — the committed dev config points
  at the same remote, already-seeded database the deployment uses.
