# profiles — customer and nurse profiles

> Client seam `client/src/services/profiles/` · `USE_PROFILES_MOCK = false` (**real**) · 7 server ops
> Last verified: 2026-07-30 against commit `d3ec723` and swagger.v1.json (2026-07-29).

One client domain over two server controllers, because the client's profile screens are one feature with
two actor variants. Identity and roles come from [auth.md](auth.md)'s `/me`, not from here.

## Endpoints

| Method | Path | Verdict |
| --- | --- | --- |
| GET | `/api/v1/customer_profiles/me` | wired |
| POST | `/api/v1/customer_profiles/upsert` | wired |
| POST | `/api/v1/customer_profiles/avatar` | **unwired** — the client's avatar upload calls the nurse route only |
| GET | `/api/v1/nurse_profiles/me` | wired |
| POST | `/api/v1/nurse_profiles/upsert` | wired |
| POST | `/api/v1/nurse_profiles/avatar` | wired |
| POST | `/api/v1/nurse_profiles/set_accepting_bookings` | wired |

All `[Authorize]`. No phantoms.

> **`customer_profiles/avatar` exists and is unused.** The nurse avatar upload is wired; the customer one
> is not, so a customer cannot set a photo even though the endpoint is live. A one-line client gap, not a
> contract gap.

## Shape rules the JSON does not express

- **Avatar upload is `multipart/form-data`.** This is the one place the client must **not** set
  `Content-Type` — `clientFetch` detects a `FormData` body and lets the browser write the multipart
  boundary. A manual JSON content-type there breaks the upload. See
  [../api-contract.md](../api-contract.md).
- **`avatarUrl` is served by the object-storage seam.** `Seams:ObjectStorage:Provider` = `local` (default,
  writes under `RootPath`) or `s3`. In the deployment `RootPath` is a **named docker volume** — without it,
  uploads vanish on the next `up --build`. See [../topology.md](../topology.md).
- **`isAcceptingBookings` is a real toggle with real consequences.** It is one of the four conditions the
  search index's `is_searchable` requires — flipping it off removes the nurse from discovery. See
  [search.md](search.md).
- **`isVerified` on `NurseProfileDto` is derived, never writable.** It is written **only** by the
  verification finalize transaction when the aggregate reaches `approved`. Never set it from a profile
  write. See [verification.md](verification.md).
- **`averageRating` / `totalReviews` / `totalCompletedBookings` are recomputed from source**, not
  incremented. A moderation change that hides a review recomputes the aggregate. See
  [reviews.md](reviews.md).
- **`specializationsJson` is a string containing JSON**, like the other `*Json` fields on this wire.
- `preferredLanguage` on `CustomerProfileDto` (REQ-007, delivered) alongside the name update.
- `defaultEmergencyContactName`/`Phone` are the customer-level fallback used when a booking supplies none.

## Enums

None of its own. `educationLevel` and `educationField` are free strings. Gender lives on the user
(see [auth.md](auth.md)), not on the profile.

## Open REQs

None. REQ-006 (avatar/object-storage upload route) and REQ-007 (name + preferred language) were both
delivered in refinement-phase-3.
