281 lines
17 KiB
Markdown
281 lines
17 KiB
Markdown
# 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.
|