create mvp path
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# 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) |
|
||||
Reference in New Issue
Block a user