# reviews — ratings, tags, moderation

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

Customer reviews of a completed booking, pre-screened and then human-moderated before they are public.
Clinical care records are a different domain — [patient-records.md](patient-records.md).

## Endpoints

| Method | Path | Auth | Verdict |
| --- | --- | --- | --- |
| GET | `/api/v1/bookings/{bookingId}/review_eligibility` | `[Authorize]` | wired |
| GET | `/api/v1/bookings/{bookingId}/my_review` | `[Authorize]` | wired |
| POST | `/api/v1/bookings/{bookingId}/review` | `[Authorize]` | wired |
| GET | `/api/v1/nurses/{nurseProfileId}/reviews` | **anonymous** | wired · paginated (**`page`/`pageSize`**) |
| GET | `/api/v1/nurses/{nurseProfileId}/review_tags` | **anonymous** | **unwired** — the client reads tags off the review rows |
| PATCH | `/api/v1/reviews/{reviewId}/status` | `[Authorize]` | wired — the moderation decision |
| POST | `/api/v1/reviews/{reviewId}/tags` | `[Authorize]` | **unwired** — no console screen edits tags |
| GET | `/api/v1/admin/reviews/moderation_queue` | admin | wired · paginated |

No phantoms. Note `PATCH` — one of only two `PATCH` verbs in the whole API (the other is on
[partner-center.md](partner-center.md)); everything else mutates with `POST`. Note also the hardcoded
nested `admin/reviews/` route segment — see [index.md](index.md#route-shape-exceptions).

## Aggregates are recomputed, never incremented

`averageRating`, `totalReviews` and `totalCompletedBookings` on the nurse profile are **recomputed from
source** whenever a review's moderation status changes. Hiding a published review must lower the average;
an incrementing counter cannot do that correctly. Never `+= 1` a review aggregate. See
[profiles.md](profiles.md).

## The moderation gate

```
submit ──AI pre-screen──▸ pending_moderation ──admin──▸ published
                                │                          │
                          banned word hit             unpublish ──▸ hidden
                                ▼
                            rejected
```

- **A review is not public on submit.** `IReviewModerationService` pre-screens; by default clean text
  returns a human-review **flag**, keeping the gate on. `Seams:ReviewModeration:AutoApproveClean` makes
  clean text auto-publish; `BannedWords` (default `scam`, `fraud`, `کلاهبردار`) forces `reject`. Both are
  mock knobs — a real classifier ignores them.
- **A low rating raises a support alert** (`low_rating`), linked as `lowRatingAlertId` on the queue item.
  See [admin.md](admin.md).
- **`unpublish` is a distinct action from `hide`** in the client's `ModerationAction` union even though both
  land on `hidden` — the audit trail records which was chosen.

## Shape rules the JSON does not express

- **`ReviewEligibilityDto` is `{ canReview, reason }`** and the reason is a **code**, not a message:
  `not_completed` `already_reviewed` `not_owner` `not_found`. The client maps it to an i18n key. Eligibility
  is server-decided — the client must not infer it from booking status.
- **The author is masked** (REQ-026, confirmed): a public review carries `authorMasked`, never a full name.
  This is a privacy decision, not a display choice.
- **`tagCodes` is on `ModerationQueueItemDto`** (REQ-037, delivered) so the queue shows what the reviewer
  tagged without a second fetch.
- **Review tags are codes; labels are i18n keys.** Never render a tag code, and never build a display
  string from one.
- `moderationReason` is admin-facing free text and is **not** returned on the public review read.
- The public reviews list is **anonymous and paginated with `page`/`pageSize`** (camelCase — unlike
  `search/nurses`, which uses `page_size`). See [../api-contract.md](../api-contract.md#pagination).

## Enums

| Vocabulary | Values |
| --- | --- |
| `ModerationStatus` | `pending_moderation` `published` `hidden` `rejected` |
| `ModerationAction` | `publish` `hide` `reject` `unpublish` |
| `ReviewIneligibilityReason` | `not_completed` `already_reviewed` `not_owner` `not_found` |

`ModerationStatus` and `ModerationAction` are both defined in
`Entities/Reviews/ReviewModerationStatus.cs` and verified identical to the client's unions — the file
carries the four states and the four actions together.

## Open REQs

| REQ | Status | Effect |
| --- | --- | --- |
| REQ-026 | delivered | Eligibility + my-review-for-booking reads, and masked-author confirmation |
| REQ-037 | delivered | `tagCodes` on the moderation queue item |
| REQ-040 | open | No `topReviewTag` on the search index row — the C2 card renders without the tag chip. Owned by [search.md](search.md) |
